I. Article original▲
Le blog KDAB est rédigé par les ingénieurs de KDAB s'occupant des formations, de la consultance ainsi que du développement (de Qt et de produits additionnels). Vous pouvez trouver les versions originales.
Cet article est une traduction de l'article original écrit par Thomas McGuire paru le 4 février 2013.
Cet article est une traduction de l'un des articles en anglais écrits par KDAB. Les éventuels problèmes résultant d'une mauvaise traduction ne sont pas imputables à KDAB.
II. Récapitulatif▲
Pour reprendre, un petit exemple de liaison :
text
:
"Window Area: "
+
(parent.width *
parent.height)
Chaque liaison, comme celle-ci, est en fait une fonction JavaScript évaluée à l'exécution par le moteur V8. Le résultat est la valeur de retour de la fonction, valeur qui sera assignée à la propriété text. V8 ne connaît rien aux objets Qt ou aux propriétés : lorsqu'il en rencontre, il demande au moteur QML une référence sur le contexte ainsi qu'une sur l'objet afin d'effectuer la résolution. Ces références mémorisent les propriétés utilisées lors de l'évaluation des liaisons, ces dernières sont ainsi automatiquement connectées aux signaux de changement (par exemple widthChanged()) de chaque propriété ainsi qu'au slot qui les réévalue.
Ces rappels faits, il est temps de s'intéresser aux différents types de liaisons qui existent.
III. Les différentes liaisons existantes▲
Le dernier article a expliqué que chaque liaison est représentée par une instance de la classe QQmlBinding. C'était en fait une grosse simplification. Avoir une instance de QQmlBinding pour chaque liaison serait vraiment trop coûteux. Il y a des centaines, si ce n'est des milliers, de liaisons dans une application QML type : les liaisons doivent, de ce fait, être légères. De plus, chaque liaison est compilée séparément durant le chargement d'un fichier QML, les performances seraient désastreuses s'il fallait appeler le compilateur V8 autant de fois qu'il y a de liaisons durant le chargement.
III-A. QV8Bindings▲
Pour régler le problème de performance des QQmlBinding, il existe une autre classe gérant les liaisons et maladroitement nommée QV8Bindings. QV8Bindings est l'ensemble des liaisons existantes dans un fichier QML, se composant d'un tableau de QV8Bindings::Binding, des structures bien plus légères. Les développeurs de QML ont fait de remarquables efforts quant à la minimisation de l'empreinte mémoire de cette structure ; ils ont même tenu compte du fait que les deux derniers bits d'un pointeur sont inutilisés de par l'alignement pour y stocker des drapeaux (la classe QFlagPointer est proposée à cet effet). Ainsi, QV8Bindings::Binding n'occupe que 64 octets.
Le gros avantage de QV8Bindings vis-à-vis de QQmlBinding est que toutes les liaisons sont compilées d'une seule traite, le compilateur V8 n'est ainsi appelé qu'une seule fois. Dans la méthode QQmlCompiler::completeComponentBuild(), lors de la compilation d'un fichier QML, toutes les liaisons sont concaténées ensemble dans un grand fichier JavaScript, et sauvegardées dans un QQmlCompiledData (une structure qui contient tout type de données compilées provenant d'un fichier QML). Lors de la première instanciation du fichier QML, le compilateur V8 est appelé, étape qui se produit dans QV8Bindings::QV8Bindings(). Le programme résultant de la compilation est ensuite stocké dans un QQmlCompiledData et la source est oubliée. À l'instanciation suivante du même fichier QML, le moteur d'exécution QML réutilise le QQmlCompiledData précédemment généré et n'a pas besoin de recompiler les liaisons. Ce n'est pas le cas avec les QQmlBinding, qui ont besoin d'être recompilées à chaque nouvelle instanciation d'un fichier QML.
Pour résumer : étant donné que QV8Bindings empaquette toutes les liaisons d'un
fichier QML, il utilise bien moins de mémoire pour chaque liaison et peut compiler toutes les
liaisons en une seule passe.
Très bien, mais qu'est-ce qui justifie alors l'utilisation de QQmlBinding, pourquoi cette classe existe-t-elle encore ? Dans certains cas, les liaisons ne peuvent pas être partagées, par exemple quand elles utilisent des fermetures ou eval(). Dans ce cas, chaque fonction de liaison nécessite son propre contexte et ne peut donc pas être compilée avec les autres liaisons qui partagent le même contexte. Dans ces cas rares et spéciaux, chaque liaison aura sa propre instance de QQmlBinding. Le choix du type de liaison à utiliser est effectué lors de la compilation du fichier QML avec la méthode QQmlCompiler::completeComponentBuild(). Là, un SharedBindingTester est utilisé pour vérifier les liaisons qui feront partie des QV8Bindings et celles qui auront leur propre QQmlBinding. SharedBindingTester est un visiteur pour l'arbre syntaxique JavaScript. Dans le code, le SharedBindingTester teste si la liaison est sûre, ce qui permet d'éviter d'évaluer plusieurs fois une même liaison lors de l'évaluation d'un fichier QML ; cette technique est fort bien décrite dans le message du commit de cette optimisation.
Pour que le code autour de QML reste simple, les classes QQmlBinding et QV8Bindings::Binding héritent de QQmlAbstractBinding.
III-B. QV4Bindings▲
Si vous regardez le code du moteur QML, vous aurez remarqué l'existence de la classe QV4Bindings,
qui se trouve être une sous-classe de QQmlAbstractBinding. Encore un autre type de liaison ?
À quoi sert cette classe ? À l'instar de
QV8Bindings, il s'agit d'un ensemble de liaisons d'un fichier QML. À la
différence de QV8Bindings, QV4Bindings ne stocke que les liaisons considérées comme
« optimisées », appelées de façon confuse
« compilées ». Certaines liaisons peuvent être optimisées, elles feront
dans ce cas partie du QV4Bindings ; d'autres ne le peuvent pas et n'en feront pas partie.
Quelle est donc cette optimisation ? Les liaisons V4 ne sont pas évaluées par le moteur
V8, elles sont compilées en code intermédiaire et exécutées
par un interpréteur. Ce compilateur n'est pas capable de traiter directement le code
JavaScript, simplement à cause du temps de compilation du JavaScript qui ne rend pas cette
technique possible dans tous les cas de figure.
Pourquoi utiliser du code intermédiaire ? Après tout, le moteur V8 compile vers du code natif, n'est-ce pas plus rapide qu'un interpréteur de code intermédiaire ? En fait, ça ne l'est pas : les performances du moteur V8 sont dégradées lors de son exécution et lors des appels au moteur QML pour résoudre le nom des objets et des propriétés. En plus de cela, il recompile parfois les fonctions à la volée, avec plus d'optimisations, lorsqu'une fonction est appelée plusieurs fois. Tout cela fait que les performances sont trop dégradées dans le contexte d'une application QML, qui comporte généralement énormément de fonctions de liaison d'une seule ligne. Vous trouverez les résultats d'un test de performance prévu pour les DevDays. Le test fait évaluer au moteur QML une simple liaison plusieurs centaines de fois. La liaison est assez simple pour être traitée par le compilateur V4. Pour effectuer une comparaison vis-à-vis du moteur V8, on utilise la variable d'environnement QML_DISABLE_OPTIMIZER=1, afin de complètement désactiver les liaisons V4.
Comme vous le constatez sur ce graphique, le code intermédiaire généré par le moteur v4 est vraiment plus rapide que celui de v8 dans ce cas précis.
En interne, V4 est une machine à registres illimités. Comme un processeur, il a des registres permettant de stocker des valeurs temporaires. À la différence d'un processeur, il ne charge ni ne stocke les valeurs depuis la mémoire, mais depuis les propriétés des QObject. En utilisant la variable d'environnement QML_BINDINGS_DUMP=1, on étudie cette simple liaison :
text
:
parent.width *
parent.height
La trace d'exécution sera :
Program.bindings
:
2
Program.dataLength
:
92
Program.subscriptions
:
4
[SNIP of other, unrelated bindings)
160
14
:
15
:
176
Block Mask(1
)
192
LoadScope ->
Output_Reg(0
)
208
FetchAndSubscribe Object_Reg(0
) Fast_Accessor(0x7f05f6e51060
) ->
Output_Reg(0
)
Subscription_Slot(1
)
272
FetchAndSubscribe Object_Reg(0
) Fast_Accessor(0x7f05f6e51090
) ->
Output_Reg(0
)
Subscription_Slot(2
)
336
LoadScope ->
Output_Reg(1
)
352
FetchAndSubscribe Object_Reg(1
) Fast_Accessor(0x7f05f6e51060
) ->
Output_Reg(1
)
Subscription_Slot(1
)
416
FetchAndSubscribe Object_Reg(1
) Fast_Accessor(0x7f05f6e510a0
) ->
Output_Reg(1
)
Subscription_Slot(3
)
480
MulNumber Input_Reg(0
) Input_Reg(1
) ->
Output_Reg(0
)
496
ConvertNumberToString Input_Reg(0
) ->
Output_Reg(1
)
512
Store Input_Reg(1
) ->
Object_Reg(0
) Property_Index(42
)
Les propriétés width et height sont chargées dans les registres 0 et 1, qui sont ensuite multipliés entre eux, le résultat étant stocké dans la propriété text (qui s'avère être la propriété numéro 42 de la classe QQuickText). L'instruction FetchAndSubscribe ne se contente pas de charger une propriété, mais s'abonne aussi au signal notifiant des changements, ce qui est nécessaire pour les mises à jour automatiques des liaisons. Dans le code « assembleur » du dessus, vous pourrez noter un avantage : le compilateur V4 résout les objets et propriétés lors de la compilation et stocke les index des propriétés dans le code intermédiaire. Ainsi, lors de l'exécution, il n'est pas nécessaire de chercher les propriétés par leur nom, il est possible de les atteindre directement par leur index. Au lieu de cela, le moteur V8 doit appeler l'objet QML ainsi qu'une référence sur l'objet afin de résoudre le nom de l'objet ainsi que celui des propriétés, ce qui est bien évidemment source de ralentissements. Le désavantage est que le moteur V4 ne peut pas utiliser directement les objets dynamiques, par exemple ceux exportés du C++ par setContextProperty(). Une liaison contenant un tel objet dynamique fera partie de QV8Bindings.
III-C. Résumé des types de liaisons existantes▲
Ainsi, il existe trois types de liaisons, toutes héritées de QQmlAbstractBinding :
- QV4Bindings::Binding ;
- QV8Bindings::Binding ;
- QqmlBinding.
Les liaisons V4 sont les plus rapides étant donné qu'elles font appel à un moteur de code intermédiaire personnalisé. Les liaisons QV8 et QQmlBinding utilisent le moteur JavaScript V8 pour l'évaluation. Quoi qu'il en soit, QV8Bindings empaquette toutes les liaisons en une seule afin de les compiler en une passe, alors que les QQmlBindings sont compilés individuellement à chaque instanciation d'un composant QML.
Voici un exemple (absurde) utilisant les liaisons :
import
QtQuick 2.0
Rectangle
width
:
360
height
:
360
Text
{
anchors.centerIn
:
parent
text
:
parent.width *
parent.height
font.pointSize
:
eval("14"
)
font.wordSpacing
:
parent.width >
10
? 90
:
~
parent.width
}
}
En utilisant QML_COMPILER_DUMP=1, vous vous rendrez compte que le compilateur QML utilise STORE_COMPILED_BINDING deux fois, une fois STORE_V8_BINDING ainsi que STORE_BINDING une fois aussi.
STORE_BINDING sert pour QQmlBinding, cela est utilisé pour font.pointSize étant donné que les liaisons utilisent eval() et ne peuvent de ce fait pas être partagées.
Les liaisons pouranchors.centerIn et text sont toutes les deux des liaisons V4 (instruction STORE_COMPILED_BINDING instruction, classe QV4Bindings::Binding).
Pour finir, font.wordSpacing est un QV8Bindings::Binding standard (instruction STORE_V8_BINDING). Le compilateur et interpréteur de code intermédiaire V4 est assez intelligent pour se débrouiller avec l'opérateur ternaire, mais l'opérateur complément n'est pas encore implémenté, c'est pourquoi le compilateur QML choisit d'utiliser les liaisons V8 à la place.
IV. Remerciements▲
Au nom de toute l'équipe Qt, j'aimerais adresser le plus grand remerciement à KDAB pour nous avoir autorisés à traduire cet article !
Je tiens à remercier Thibaut Cuvelier pour ses conseils ainsi que ClaudeLELOUP pour sa relecture.