Qt Graphics et performance - Le coût des commodités

Image non disponible

En général, quand on souhaite implémenter une fonctionnalité graphique, plusieurs choix de conception sont possibles. Les utilisateurs de Qt peuvent choisir de créer leur propre QProxyGraphicsItem, de créer un QGraphicsItem ou encore de créer un widget personnalisé.

Ce choix conceptuel n'est pas sans conséquence en termes de performances. Dans cet article, l'auteur présente plusieurs méthodes pour implémenter un exemple simple de clavier virtuel et montre les conséquences de ce choix. Il en conclut que les méthodes les plus simples à réaliser ne sont pas les plus performantes.

Cet article est une traduction autorisée de Qt Graphics and Performance ? The Cost of Convenience de Gunnar.

N'hésitez pas à commenter cet article !
9 commentaires Donner une note à l'article (5)

Article lu   fois.

Les trois auteurs

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. L'article original

Le site Qt Labs permet aux développeurs de Qt de présenter les projets, plus ou moins avancés, sur lesquels ils travaillent.

Nokia, Qt, Qt Quarterly et leurs logos sont des marques déposées de Nokia Corporation en Finlande et/ou dans les autres pays. Les autres marques déposées sont détenues par leurs propriétaires respectifs.

Cet article est la traduction de l'article Qt Graphics and Performance ? The Cost of Convenience de Gunnar paru dans Qt Labs.

II. Introduction

Le temps est venu pour un nouveau post sur Qt Graphics et les performances. Cette fois, le sujet est de savoir comment sont liées les commodités offertes par Qt Graphics et les performances, en particulier dans le contexte de QGraphicsView. Mon objectif est d'illustrer que le moyen de parvenir à des rendus graphiques rapides est de rassembler vos appels de dessin QPainter aussi étroitement que possible. Plus les choses sont faites à haut niveau, plus ça sera lent.

Pour illustrer cela, j'ai mis en place un clavier virtuel. Certes, la disposition ni très commune, ni utilisable, mais le rendu est le point important ici, pas la fonctionnalité. Le code source complet est disponible et le résultat ressemble à ceci :

Image non disponible


J'ai implémenté le clavier en utilisant trois approches différentes. La première utilise les proxy widgets (QGraphicsProxyWidget), une autre utilise les éléments graphiques (QGraphicsWidget), la dernière utilise l'ensemble de la vue dans un seul élément graphique. De plus, j'ai ajouté un certain nombre d'options pour adapter les différentes propriétés, comme afficher le texte ou non. J'ai mesuré les performances sur un N900 plutôt qu'un ordinateur de bureau parce que la différence devient plus importante sur un petit appareil. Sur un ordinateur de bureau, il est facile de se laisser berner du fait que la plupart des choses sont réalisées en quelques microsecondes. Ce n'est que lorsque l'application est entièrement finalisée que l'on remarque que les choses ne sont pas aussi fluides que dans le prototype, mais trop de travail a été investi dans la conception actuelle pour le perdre pour refaire une application donnant une impression super-fluide.

III. QGraphicsProxyWidget

Puisque nous avons implémenté une série de boutons cliquables, un point de départ commode et naturel est d'utiliser une classe de boutons existante, comme QPushButton. Il implémente déjà la logique de l'interaction clavier/souris et les signaux pour cliquer et toutes sortes d'autres fonctionnalités utiles. Pour obtenir des widgets dans QGraphicsView, nous utilisons un QGraphicsProxyWidget. Pour faire que le test soit "équitable", j'utilise un QWidget ordinaire qui dessine simplement une pixmap et affiche un texte. Si j'avais utilisé l'API pour les styles, ces chiffres auraient été encore pires.

Image non disponible
Nombre de millisecondes passées par trame, y compris la copie à l'écran, lors de l'utilisation QGraphicsProxyWidgets. Plus le chiffre est faible, mieux c'est !


Si nous regardons la version simple "-proxywidgets", le moteur le plus rapide a été le moteur Raster, qui tourne à 26 ms par image. Si je voulais faire glisser ce clavier à l'écran, j'aurais 16 ms de disponible si je veux qu'il tourne à 60 FPS et 33 ms disponibles si je veux le faire à 30 FPS. Lorsque chaque image prend 26 ms, je peux à peine monter à 30 FPS, mais avec très peu de marge, donc si un autre processus consomme du temps CPU, ce nombre est aussi un peu difficile à atteindre. Donc, pas très bon (en fait, les chiffres exacts dans les graphiques sont répertoriés dans un commentaire dans le haut du fichier .cpp ci-dessus).

La première chose que j'ai remarquée avec cette approche est que chaque bouton dispose désormais d'un fond gris. C'est bien sûr le fond du widget. Un QWidget intégré dans QGraphicsView sera traité comme un top-level widget(1) et devra donc dessiner son arrière-plan. J'ai ajouté une option "-no-widget-background", qui ajoute l'option Qt::WA_NoBackground sur le widget. Cela diminue la vitesse de rendu des trames jusqu'à 22 ms, soit 4 ms sauvés par trame, juste en modifiant une option. Ce n'est pas trop mal, mais c'est encore assez loin d'être impressionnant.

J'ai déjà précisé que le rendu de texte n'est pas aussi rapide que nous le voudrions, donc, juste pour comparer les performances sans texte, j'ai ajouté une option "-no-text" dans le test. Cela diminue les résultats du moteur Raster jusqu'à 13 ms. C'est très bien et en dessous du seuil de 16 ms nécessaire pour atteindre 60 FPS, mais seulement avec une petite marge. Et je ne dessine aucun texte ! Avant de terminer avec cette approche, je vais ajouter la mise en cache des éléments. En définissant le mode de cache à ItemCoordinateCache sur chaque bouton, je mets en cache la pixmap d'arrière-plan et le texte dans une pixmap unique. Cela diminue les résultats du moteur Raster jusqu'à 8,5 ms et ça commence à être acceptable. Mais avec un coût élevé de la mémoire... Dans mon cas d'utilisation d'origine, j'avais une seule pixmap partagée pour tous les arrière-plans de bouton, mais, maintenant, j'en ai une par bouton.

Vous pouvez remarquer qu'il y a une grande différence de temps entre dessiner la pixmap d'un élément mis en cache et le proxy widget qui dessine une pixmap. Un coût supplémentaire ajouté au proxy widget est que le QPainter est recréé et initialisé pour chaque bouton dans les évènements paintEvent. En outre, comme je l'ai mentionné dans mon post précédent, Vue d'ensemble, vous devriez vous souvenir que j'ai dit que chaque widget a un système de découpage et qu'il y a des frais généraux liés à l'appel de paintEvent. Pour les éléments dans une QGraphicsView, il existe déjà un QPainter et je n'ai pas besoin de découpage, ni de tout ce qui se passe en arrière-plan. Lorsque nous activons la mise en cache des éléments, nous ne quittons pas le contexte de la vue graphique et nous n'entrons pas dans le contexte du widget. Ce changement est coûteux, donc, en ne basculant pas dans le contexte du widget, nous économisons beaucoup de temps.

Donc, s'il y a une leçon à tirer, c'est que QGraphicsProxyWidget doit être utilisé avec une extrême prudence. Si vous en avez vraiment besoin, utilisez-le au minimum.

IV. QGraphicsWidget

Si les proxy widgets sont trop lents pour être utilisables dans ce scénario, la meilleure chose est d'utiliser un QGraphicsWidget. Il s'agit d'une sous-classe de QObject et de QGraphicsItem, ce qui nous donne des signaux, des slots et des propriétés, mais sans être un QWidget et donc encore assez léger. Les chiffres sont les suivants :

Image non disponible
Nombre de millisecondes passées par trame, y compris la copie à l'écran, lors de l'utilisation QGraphicsWidgets. Plus le chiffre est faible, mieux c'est !


Par comparaison avec l'approche basée sur les proxy widgets, nous allons commencer avec de meilleurs résultats, avec un temps de 13 ms par image pour le moteur Raster, 20 ms pour le moteur OpenGL et 22 ms pour X11. En dessous de cette ligne apparaît une nouvelle : "-no-indexing -optimize-flags". QGraphicsView, par défaut, met tous les éléments d'une vue dans un arbre BSP (partition binaire de l'espace) pour une recherche rapide, ce qui est bénéfique lorsque la scène contient de nombreux éléments et que vous avez souvent besoin de trouver des éléments qui appartiennent à une petite portion de la scène. Dans notre scénario de test, nous réalisons toujours une mise à jour complète de la vue, il n'y a pas de bénéfice à l'indexation, de sorte qu'il puisse être désactivé en appelant scene->setItemIndexMethod(QGraphicsScene::NoIndex). Avoir un arbre BSP est le comportement par défaut, car une vue graphique est initialement destinée à être une scène statique pour de nombreux éléments. Le cas d'utilisation le plus général aujourd'hui est d'avoir peu d'éléments (quelques centaines au maximum) qui ont tendance à bouger beaucoup. Pour cette raison, c'est toujours une bonne idée d'essayer de désactiver l'arbre BSP et de voir si cela fait une différence dans les performances. Si cela peut aider, alors laissez-le désactivé.

Je sais aussi que les éléments travaillent correctement, ce qui signifie qu'ils ne changent pas la zone de découpage, ne déplacent pas le QPainter, ne changent pas le mode de composition ou ne modifient pas des états qui se propageraient aux autres éléments. Cela signifie que je peux en toute sécurité activer l'optimisation DontSavePainterState. En fait, selon une vieille habitude, j'active toutes les options d'optimisation possibles. Je considère la possibilité de les désactiver seulement si mon code de dessin commence à avoir l'air bizarre, au point que je préfère corriger le code de mes dessins et garder les optimisations actives. Désactiver l'indexation et l'optimisation permet de gagner 2 ms par image dans pour toutes les interfaces de rendu, ce qui en vaut vraiment la peine.

Si je ne dessine pas de texte, le rendement est environ deux fois plus rapide. Encore une fois, nous voyons que le dessin des textes représente un coût énorme. Nous travaillons sur une API pour résoudre ce problème et vous aurez plus d'informations quand ça sera fait (fait dans Qt 4.7 : voir QStaticText et articles associés : Du texte rapide et La folie est de mettre en forme à nouveau le même texte et espérer un résultat différent). Vous pouvez remarquer que l'activation de la mise en cache des éléments fait un peu chuter les performances par rapport au cas avec "-no-text". Il n'y a pas beaucoup de frais généraux à l'intérieur de QGraphicsView avec cette approche. Une raison probable de la baisse de performances est que la lecture à partir de sources multiples de la mémoire (multiples pixmaps) se traduit par une utilisation importante du cache, par rapport à une approche directe qui dessine la même pixmap encore et encore.

V. Un élément ButtonView

Dans mon post précédent, j'ai mentionné brièvement qu'il y a aussi de légers frais généraux associés à l'utilisation d'un QGraphicsItem. Avant d'appeler la fonction paint(), le QPainter est transformé dans le système de coordonnées de l'élément et son état est sauvegardé. Si l'élément trace un grand polygone, le coût de cette mise en place ne peut être ignoré, mais, lorsque l'on dessine simplement une pixmap et un texte de quelques pixels, alors il peut être utile d'envisager de s'en passer. Dans l'esprit de « plus le code de dessin est direct, plus il est rapide », j'ai implémenté le clavier comme un seul élément. Les chiffres sont comme suit :

Image non disponible
Nombre de millisecondes passées par trame, y compris la copie à l'écran, lorsque vous utilisez un seul élément. Plus le chiffre est faible, mieux c'est !


Le moteur Raster descend maintenant à 10 ms, ce qui représente une amélioration d'une milliseconde par rapport à l'approche basée sur QGraphicsWidget avec toutes les optimisations activées, donc, même si les éléments graphiques sont moins chers que les widgets, ils coûtent encore un peu. Le clavier est maintenant dessiné dans une courte boucle et la grande différence de performance provient du fait que les éléments de la scène ont une transformation qui leur est associée. Avant d'appeler paint(), une transformation est effectuée pour faire correspondre le QPainter au système de coordonnées local du widget. Cela provoque un changement d'état dans le moteur de rendu. Pour chaque bouton, on dessine une pixmap 32x32, ce qui implique un mélange du canal alpha de 1024 pixels, suivi par la mise en forme du texte et le dessin d'un seul caractère. En faisant cela, nous économisons environ 10 % du temps en n'ayant pas d'appel à QPainter::translate() au cours du rendu, ce qu'il faut garder en mémoire. En activant les options d'optimisation et la désactivation de l'indexation, le moteur Raster gagne un peu de temps, utiliser ces options est donc toujours une bonne idée.

Vous avez sans doute remarqué qu'il y a un ensemble de résultats, nommé "cheat", pour OpenGL. J'ai hésité à inclure ces données, parce que cela utilise une API privée qui ne respecte pas, et je veux vraiment dire QUI NE RESPECTE PAS, les règles de compatibilité binaire. Vous ne pouvez pas l'utiliser dans votre application. Nous ajouterons une API publique pour l'utiliser dans le futur, espérons dans 4.7. Jusque là, attendez. Dans l'intérêt de montrer les pistes de réflexion en interne, je pense que je pouvais vous le montrer.

OpenGL est vraiment idéal pour accélérer les graphismes, mais sa façon de travailler ne correspond pas parfaitement au fonctionnement de Qt. OpenGL est vraiment bon pour travailler sur un petit nombre de grands ensembles de données de triangles et les dessiner, mais il est moins performant pour dessiner beaucoup de petites choses, comme les arrière-plans des boutons, les icônes, les éléments de texte simple, etc. Toutefois, les arrière-plans de tous les boutons sont la même pixmap, donc que se passerait-il si je pouvais dire à QPainter de dessiner la même pixmap à plusieurs endroits à la fois ? Dans OpenGL, cela correspondrait à la création d'une texture et d'un tableau de coordonnées de sommets et de textures et à dessiner les 40 pixmaps en une seule fois. Cela correspond beaucoup mieux à la façon dont OpenGL est fait pour travailler. Le résultat est que le dessin des boutons baisse de 5,2 ms à 3,9 ms. Naturellement, plus la pixmap demande de temps pour être dessinée et plus la pixmap fournie est petite, plus le bénéfice d'utiliser des commandes comme celle-ci sera grand.

Il y a une deuxième option d'OpenGL pour le cas du ButtonView, qui est le "-ordered". Cela a été ajouté après que Tom m'ait signalé que le test fait une mise à jour du programme shader pour chaque appel de QPainter. Dans l'implémentation par défaut de ButtonView, nous faisons :

 
Sélectionnez
for (int i=0; i < m_rects.size(); ++i) {
    p->drawPixmap(m_rects.at(i), *theButtonPixmap); 
    p->drawText(m_rects.at(i), Qt::AlignCenter, m_texts.at(i)); 
}

Puisque les pixmaps utilisent un shader et que le rendu du texte en utilise un autre, le pipeline doit basculer entre les deux shaders et se réinitialiser tout le temps, ce qui baisse le temps de rendu à 16 ms par image. Pour voir si cela fait une différence, j'ai ajouté une autre option de rendu, "-ordered", où je dessine d'abord toutes les pixmaps , puis tout le texte :

 
Sélectionnez
for (int i=0; i < m_rects.size(); ++i) 
    p->drawPixmap(m_rects.at(i), *theButtonPixmap); 
for (int i=0; i<m_rects.size(); ++i) 
    p->drawText(m_rects.at(i), Qt::AlignCenter, m_texts.at(i));

Cela empêche les mises à jour des shaders et réduit le temps de rendu par image jusqu'à 13 ms, alors ça en vaut vraiment la peine.

VI. Bilan

Image non disponible
Nombre de millisecondes passées par trame, y compris la copie à l'écran, pour les proxy widget, les widgets graphiques et un seul widget. Plus le chiffre est faible, mieux c'est !


OpenGL s'en sort plutôt mal dans ce test, j'ai été un peu déçu de le constater, mais ça a permis à Tom de partir dans une frénésie d'optimisations. Nous espérons donc éliminer une partie des frais généraux constants. Il faut dire aussi que lorsque vous utilisez le système graphique OpenGL, nous utilisons le multisampling par défaut, ce qui augmente le temps de rendu sur la N900 d'environ 30 %. Un QGLWidget natif donnerait donc des résultats légèrement supérieurs. Un autre aspect d'OpenGL est qu'il utilise une puce dédiée de faible puissance, de sorte que, même s'il fonctionne à la moitié de la vitesse pour ce cas d'utilisation particulière, il utilise également beaucoup moins la batterie et il peut être quand même un bon choix. OpenGL sera également nettement mieux que les moteurs Raster et X11 dans le cas où les pixmaps deviennent de plus en plus grandes ou si le contenu du bouton est un peu plus avancé, par exemple avec un gradient horizontal.

Les meilleurs chiffres sont obtenus sans aucun doute dans le cas du ButtonView, où tout le contenu est rendu sous la forme d'un seul élément, ce que je voulais mettre en évidence dans ce post. L'élément ButtonView est également ouvert pour d'autres optimisations telles que le regroupement en paquets de primitives. Nous n'avons pas beaucoup de fonctions utilisant des paquets de primitives dans QPainter aujourd'hui, seulement drawRects(), drawLines() et drawPoints(), mais nous envisageons d'en ajouter d'autres. Nous ne sommes pas encore sûrs de savoir à quoi l'API devra ressembler.

La ligne du bas montre que la façon dont Qt est utilisé définit ses performances : d'une part, il peut y avoir un moyen facile et pratique de réaliser le travail souhaité, mais de manière non optimale ; d'autre part, il peut y avoir une implémentation plus complexe qui donne de meilleures performances. Je n'essaie pas de suggérer que vous devez utiliser l'une ou l'autre. Il y a beaucoup de bonnes raisons pour choisir l'une ou l'autre. Mais j'espère que j'ai montré que certaines fonctionnalités ont un coût et qu'il faut garder à l'esprit quel est l'objectif lorsque l'on évalue et choisit un design.

Je vais conclure par une question. Si vous deviez implémenter un effet de particules lorsque vous appuyez sur un bouton, quelle approche choisiriez-vous, après avoir vu les chiffres ci-dessus ?

VII. Conclusion

Au nom de toute l'équipe Qt, j'aimerais adresser le plus grand remerciement à Nokia pour nous avoir autorisé la traduction de cet article !

Merci à jacques_jean et à dourouc05 pour leur relecture !

Qt Graphics et performances
Ce qui est critique et ce qui ne l'est pas
Vue d'ensemble
Le moteur de rendu Raster
Le moteur de rendu OpenVG
Le moteur de rendu OpenGL
Le coût des commodités
Du texte rapide
Génération de contenu dans des threads
La folie est de mettre en forme le même texte encore et espérer un résultat différent
Velours et QML Scene Graph

Un top-level widget est un widget qui n'est pas inclus dans un autre et qui s'affiche donc dans une fenêtre native (QFrame, QDialog, etc.), au contraire d'un widget, qui sera inclus dans un autre widget.

  

Copyright © 2010 Gunnar. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.