I. L'article original▲
Qt Labs est un site géré par les développeurs de Qt. Ils y publient des projets, des idées propres et des composants afin d'obtenir les retours d'information sur les API, le code et les fonctionnalités ou simplement pour partager avec nous ce qui les intéresse. Le code que vous y trouverez peut fonctionner tel quel mais c'est sans aucune garantie ni support. Voir les conditions d'utilisation pour plus d'informations.
Nokia, Qt, Qt Labs 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 When being wrong is right de Thiago Macieira paru dans Qt Labs.
II. Introduction▲
J'aime le mois de juillet, il y a tellement de bonnes choses dans ce dernier, comme ne plus avoir de neige ni de nuit à Oslo (techniquement on peut, mais nous n'avons droit qu'à une ou deux heures de lumière par jour). L'autre bonne chose, comme dirait Harald : « les vacances ». Bien que j'aime beaucoup les miennes, les vacances auxquelles je pense sont celles où toute la Finlande est en vacances. Cela signifie moins d'emails, moins de choses à gérer. D'un autre côté, cela signifie qu'on aurait plus de temps à consacrer à la libre réflexion.
Il y a quelques semaines, je discutais sur le chat irc #meego (irc://irc.freenode.net/meego) avec Arjan van de Ven et d'autres personnes sur les timers. Il est venu avec l'idée de suggérer une nouvelle API Qt qui rejoint les exigences du glib g_timeout_source_new_seconds et de son API. Arjan est la référence en ce qui concerne la sauvegarde de l'alimentation des systèmes Linux. Il est l'auteur du PowerTOP, TimeChart et son blog est plein d'informations sur ce sujet (malheureusement, il n'a pas l'air de blogger souvent, ce qui est regrettable).
Ce qu'il m'a expliqué, c'est que beaucoup de timers dans les applications n'ont pas besoin de beaucoup de précision. Il leur suffit d'être dans la bonne fourchette. Plus important, il est préférable pour la durée de vie de la pile, que le processeur se mette en sommeil le plus possible. Traiter par lots les réveils à l'intérieur d'une application et à travers les applications donne des résultats intéressants. On sacrifie la précision en faveur la durée de vie de la pile. Comme il me l'a expliqué, on crée des fractions dans les attributs d'une session, par la suite on ordonnance les réveils dans cette fraction de seconde, appelée aussi « perturbation ». Toutes les applications utilisent les mêmes attributs de session, donc toutes les applications calculent la même perturbation. Et, comme toutes ces valeurs restent constantes pendant toute sur la durée de la session, nous aurons des points de réveils tous espacés d'une seconde l'un de l'autre.
Pourquoi ne pas tout simplement choisir une valeur constante, au lieu de faire des calculs ? Quelque chose comme 0 ? Eh bien, la réponse est que vous voulez sauvegarder la charge de la pile de la machine, sans augmenter la congestion sur le réseau. Imaginez que vous êtes sur un réseau avec d'autres machines qui utilisent le même algorithme. Imaginez aussi que l'horloge de votre ordinateur est synchronisée avec une source externe via NTP. Si tout le monde se réveille au même moment, il est possible qu'ils essayent tous d'accéder au réseau en même temps, en causant des collisions, donc des transmissions.
Maintenant, ceci n'est pas nouveau pour nous. Je me souviens avoir discuté de l'idée d'une horloge granulaire avec Brad plus d'une fois - et probablement avec d'autres personnes, mais on ne l'a jamais implémentée, parce qu'on n'avait pas le bon algorithme.
III. Mieux que la GLib▲
Alors je me suis assis devant mon poste de travail pendant deux après-midi pour mettre en œuvre une implémentation de base, qui comprend une nouvelle classe QAbstractEventDispatcherV2 et un énumération QTimer. Ensuite j'ai amélioré l'algorithme pendant le vol pour la Finlande, pour rejoindre Akademy 2010 à Tampere. Comme toujours, c'est quand vous êtes sous batterie que vous pensez à sauvegarder vos batteries
Au lieu des deux modes que vous offre la GLib (High precision et seconds-backed), j'en ai implémenté trois, aidé par la surcharge C++ et un enum. On donne cet enum pour choisir son mode (QTimer::HighPrecision) ou pour choisir l'implémentation seconds-backed (actuellement appelée QTimer::VeryCoarse, la révision de l'API n'est pas clairement faite). Cependant, mon implémentation de l'algorithme seconds-backed ne calcule pas de perturbation, elle ne fait qu'arrondir la fraction de seconde à la seconde la plus proche (ainsi que votre intervalle).
Vous vous demandez peut être comment puis-je passer outre en utilisant une perturbation de 0, contrairement à Glib ? La différence réside dans la manière de calculer le temps dans Qt. La boucle principale de Glib utilise la fonction g_get_current_time qui retourne le temps actuel, comme QDateTime::currentDateTime de Qt, ou QDateTime::currentMSecsSinceEpoch (introduite dans la version 4.7 ). Qui sont ajustés par NTP. Cependant la boucle d'évènements Qt utilise une fonctionnalité des systèmes Unix appelée « temps monotonique », qui n'est pas ajusté par NTP et qui retourne une valeur qui jamais n'avance ni ne recule (d'où le nom monotonique, qui remplace monotone, s'avérant être très ennuyeux). L'horloge monotonique est implémentée dans la version 4.7 de Qt avec la classe QElapsedTimer.
L'autre fonctionnalité que j'ai aussi ajoutée est le nouveau mode par défaut. Pour le QTimer, j'ai choisi comme mode par défaut un algorithme adaptatif que j'ai appelé QTimer::Coarse (à nouveau, l'API n'a pas encore été revue). Ne l'oubliez pas : on nous a toujours dit depuis des années que QTimer n'est pas précis, je profite de cette occasion pour le rendre imprécis. Mais je ne vais pas passer par-dessus bord. Alors j'ai choisi une erreur absolue maximale de 5 %, ce nombre m'a donné deux limites pour l'algorithme adaptatif : 20 ms et 20 secondes.
IV. L'erreur▲
Pour 20 ms, 5 % d'erreur donnent 1 ms, donc l'algorithme adaptatif va introduire des erreurs inférieures à la granularité du calcul. Ou, si vous spécifiez une horloge inferieure ou égale à 20, on bascule tout simplement vers le mode haute-précision. Ceci signifie que toutes vos horloges de 16 ms (pour atteindre 60 fps) vont continuer à fonctionner. De la même façon, à 20 secondes, l'erreur que je peux introduire est d'une seconde, donc on bascule tout simplement vers le mode seconds-backed. Dans ce cas, toutes les horloges de 30 secondes pour les temps d'attente réseau seront arrondies à la seconde.
Entre 20 et 100 ms (à l'exception de 25, 50 et 75), on arrondit tout simplement les temps de réveil. Alors si vous demandez un intervalle de 23 ms votre code va se réveiller à 22 ms, après à 24 ms et une fois encore à 22 ms, etc. Vous aurez alors une moyenne de 26 secondes, mais ça reste toujours un peu au-dessus.
Pour les autres cas, l'algorithme va introduire jusqu'à 5 % d'erreur pour essayer d'aligner les réveils. L'algorithme que j'ai implémenté vérifie tout simplement que l'intervalle [attendu-Epsilon, attendu+Epsilon] (où attendu est le temps de réveil et Epsilon représente le taux d'erreur de 5%) contient les temps de réveils préférés. Si c'est possible de se réveiller dans une seconde complète, l'algorithme fera ainsi. Autrement, il va vérifier s'il est possible de se réveiller 500 ms après la seconde complète. Et après les autres instants...
En plus, il choisit les limites de réveil en se basant sur le multiple de votre horloge. Si vous avez un intervalle de 500 ms, il va essayer de se réveiller à un multiple de 500 ms (Donc 0 ou 500). Si c'est un multiple de 250 ms, il va essayer de se réveiller à un multiple de 250 ms (0, 250, 500 et 750). Et ainsi de suite pour 200, 100 ou 50 ms. Si votre intervalle n'est pas un multiple de 50 ms, comme dans le cas d'un intervalle de 623 ms, vous vous sentez bizarre et le code ne vous fera pas beaucoup de bien .
L'effet intéressant réside dans le fait que les horloges glissent à travers le temps. Elles glissent jusqu'à la limite voulue et vont l'atteindre dans 20 cycles (dans le pire des cas). Une fois qu'elles ont atteint la limite voulue, elles vont se synchroniser avec elle pour redevenir plutôt précises.
Prenez l'exemple suivant : maintenant, le temps est de 40120,673 secondes et vous choisissez une horloge de 400 ms, on calcule une erreur absolue maximale de 20 ms et comme temps de réveil un multiple de 200 ms. Le premier temps de réveil serait 40121,073 et le multiple le plus proche dans ce cas est 40121,000. En introduisant l'erreur, on planifie un réveil à 40121,053. Ceci est un intervalle de 380 ms. Le prochain temps d'arrêt serait à 40121,433, puis 40121,813. Après ça, on réalise que l'instant 40122,200 est à portée, alors on le choisit. Juste après, on réalise qu'on peut se réveiller précisément à des intervalles de 400 ms.
Ce n'est pas très utile pour une seule horloge, une application normale peut avoir quelques horloges en exécution, un système a plusieurs applications en exécution. Maintenant, les applications vont déclencher le réveil du noyau au même instant ; ainsi; de son cot;é le noyau peut planifier un long sommeil et réveiller le CPU d'un sommeil profond seulement en cas de besoin.
V. Du côté du noyau▲
Arjan a aussi souligné que, depuis la version Linux 2.6.28, une de ses fonctionnalités permet aux applications d'informer le noyau quand leur horloge est faible, via prctl(2) (à travers PR_SET_TIMERSLACK, qui ne semble pas être documentée) je ne suis sûr du bon moyen d'exploiter cette fonctionnalité afin de sauvegarder de la puissance. Ce que j'aimerais dire au noyau, au moment de passer au sommeil, est quelle est la faiblesse de notre horloge. Ainsi, on évite de faire un appel système de plus. Arjan m'a dit que, si nous pouvons en faire usage, il va l'ajouter au noyau.
Et j'ai essayé le code sur une application existante. En fait, j'ai été sous KDE depuis maintenant deux semaines avec les différentes versions de ce code, sans effets néfastes. En fait, le nombre de réveils par seconde dans les applications complexes comme plasma-desktop semble avoir diminué. Ces applications complexes chargent souvent des plug-ins qui effectuent des tâches indépendantes. J'ai remarqué que plasma, par exemple, lance 62 horloges de 2 secondes sur mon poste de travail (un peu moins sur l'ordinateur portable). Avec mon code, tout ça est réduit à un ou deux réveils (sur l'ordinateur portable, le démarrage est assez lent pour qu'il dépasse toujours la limite et ainsi plasma va se réveiller deux fois par seconde).
Quoi qu'il en soit, sur le titre de ce blog... quand je l'ai écrit, ça m'a vraiment rappelé « La plus grande erreur d'Einstein », qui est toujours en débat, car Einstein aurait pu avoir raison, même quand il pensait qu'il avait tort.