Articles Tagués ‘Windows’

h1

La bibliothèque des ELFs

2010.12.05

Hello,

Il y a beaucoup de choses que l’on apprend pas du tout dans les cours de programmation, et même en suivant une haute école pour une formation en tant qu’ingénieur. Heureusement les logiciels libres permettent de côtoyer des spécialistes qui ont une expérience pratique du logiciel, contrairement à de nombreux professeurs qui ne connaissent que la théorie mais ne semblent pas vraiment pratiquer et passent ainsi à côté de potentiels problèmes importants. Je pense tout particulièrement au C dans le contexte des exécutables ELF utilisés par les OS Unix-like. Bien que beaucoup d’écoles restent encore trop rattachées sur les technologies Microsoft, dans le monde des systèmes embarqués GNU/Linux est particulièrement présent. Néanmoins la formation sur le langage C n’est pas toujours très bonne. Une erreur trop courante concerne les espaces de nom utilisés par les bibliothèques.

Les fonctions "static"

Quand j’étudiais le C++ à l’école d’ingénieur on devait travailler avec un outil que je déteste absolument, c’est Rhapsody. Un générateur de code à partir d’UML. L’UML c’est très bien pour poser les idées et réfléchir sérieusement sur un programme. Mais le fait qu’un langage de programmation ne soit pas objet ou orienté-objet n’empêche pas de se donner des règles reposants sur les concepts objets. Bref, Rhapsody c’est pire que tout, il génère du code (C, C++ ou autre) à partir d’UML. Finalement le programmeur fait du dessin, des carrés, des ronds et les relie avec des flèches. Il presse un bouton et des centaines de lignes de code se génèrent. Wouhaw.. moi qui aime programmer.. Bientôt on devra se former en tant que graphiste avant de réaliser un logiciel :-).

OK vous l’aurez compris, je déteste ça. Mais là où je veux en venir c’est à une petite anecdote. On travaillait donc sur ce logiciel et un des objet était instancié une seule fois (donc en tant que Singleton). Alors je demande à l’assistant qui était présent avec nous pour répondre à nos questions:

Qu’est-ce que c’est concrètement un Singleton?

Il s’assied et m’explique ce que je sais déjà, c’est à dire que l’objet est instancié qu’une seule fois. Il me montrait via l’interface de Rhapsody comment on paramètre la classe pour être un Singleton. Et me ré-expliquait toujours la même chose. J’essayais de reformuler ma question en vain. C’est finalement en allant observer le code C généré que j’ai vu l’objet instancié en tant que variable globale statique.

5 secondes pour lire le code et comprendre, 15 minutes d’explication dans le monde "merveilleux" de Rhapsody et des formes géométriques.

Mais revenons à nos fonctions statiques

Une chose que tout le monde apprend aux cours c’est qu’une variable déclarée comme étant "static" dans une fonction, garde sa valeur entre chaque appel de fonction. Par contre je n’ai jamais eu un seul professeur qui nous ait expliqué à quoi sert une variable globale statique, ou alors une fonction statique (sauf les fonctions statiques en C++, mais ici je fais référence au C).

En fait, il est très rare de voir un professeur proposer de créer une bibliothèque pendant un cours. En principe on créer des exécutables avec un main. En C je n’ai jamais vu (en dehors des logiciels libres et des professionnels), des fonctions déclarées comme étant statiques. Que se sois du code créé par des professeurs ou par des étudiants. Pourtant l’écriture de bibliothèques demandent quelques considérations supplémentaires qui concernent les espaces de nom et plus particulièrement les ELF.

Mais tout d’abord une fonction "static" est une fonction qui n’est pas "extern" (qui est le qualificateur de type par défaut). En étant "static" la visibilité de la fonction est restreinte au fichier source où elle a été déclarée. Une fonction externe peut être atteignable depuis n’importe où dans les sources. Mais dans le cas des ELF c’est encore plus subtile que ça car elle peut être atteinte depuis n’importe quelles bibliothèques ou applications étant liées avec elle.

Les Shared Object (so) et les DLL

Sans partir dans des explications compliquées et inutiles il faut bien faire la différence entre une DLL (de chez Microsoft) et un Shared Object (qui vient du monde Unix). Les développeurs pour Windows connaissent bien:

__declspec(dllexport)

Qui est un qualificateur de type inventé par Microsoft. Il permet de spécifier les fonctions qui doivent être exportées par la DLL. Vous pouvez aussi utiliser un fichier .def qui donne la liste des fonctions à exporter. Ce principe existe aussi avec les gcc >=4 mais est rarement utilisé à ma connaissance. Quoi qu’il en soit dans tous les cas vous avez une liste de symboles exportés.

La différence qui m’intéresse c’est au runtime, lorsque le lanceur d’application doit charger les bibliothèques. Dans le cas d’une DLL, le programme va rechercher les pointeurs sur les fonctions désirées à l’aide de LoadLibrary. C’est lourd mais ça fonctionne. Dans le cas d’un "so" le fonctionnement est très différent. Le lanceur de programme de Linux va charger les bibliothèques les unes après les autres dans l’ordre où elles ont été liées. Lorsque le programme a besoin d’une fonction, c’est la première occurrence trouvée qui sera utilisée.

Concrètement

Imaginons un programme qui utilise libplayer et libvalhalla. Si je n’étais pas conscient du problème que je viens d’expliquer, dans ces deux bibliothèques j’aurais pu écrire une fonction qui a exactement le même nom (de part et d’autre) comme par exemple:

libplayer:

void foobar (int a, int b);

libvalhalla:

void foobar (int c);

Ces fonctions ne sont pas static car bien entendu j’aimerais les utiliser partout dans les projets. Alors que se passe-t-il lorsqu’on lie l’application sur ces deux bibliothèques? Notez que ces deux fonctions ne sont pas non plus déclarées dans les en-têtes "publiques" que vous distribuez à vos développeurs. Par exemple vous donnez ceci à un ami:

player.so (que vous avez compilé vous même)

Et un fichier d’en-tête:

player.h

/* libplayer header */
void libplayer_is_the_best (void);

De même avec la seconde bibliothèque:

valhalla.so
valhalla.h

/* libvalhalla header */
void libvalhalla_is_the_best (void);

Maintenant vous avez créé une application tel que:

#include <player.h>
#include <valhalla.h>

int
main (void)
{
  libplayer_is_the_best ();
  libvalhalla_is_the_best ();
  return 0;
}

Dans libplayer.so il y a le symbole "foobar", mais il existe également dans la bibliothèque libvalhalla.so. Lorsque vous liez votre application vous n’avez aucune erreur. Vous utilisez deux fonctions considérées comme publiques et qui n’ont pas du tout le même nom. Alors où est le problème? Et bien c’est très simple. Quand vous liez votre programme vous devez passer les noms des bibliothèques. Par exemple:

cas 1: gcc -lplayer -lvalhalla main.c -o main

Mais vous auriez aussi pu faire

cas 2: gcc -lvalhalla -lplayer main.c -o main

Les deux façons sont correctes mais le comportement de l’application main n’est pas du tout le même. Les deux bibliothèques utilisent la fonction foobar. Cette fonction n’a pas le même nombre d’arguments dans libvalhalla que dans libplayer et leurs comportements sont différents. Les fonctions libplayer_is_the_best et libvalhalla_is_the_best utilisent "en théorie" leurs propres fonction foobar. Et bien en réalité ce n’est pas le cas.

Dans le cas 1, le chargeur de programme va commencer par player.so, puis par valhalla.so. Lorsque la fonction libplayer_is_the_best va utiliser foobar, alors le foobar de player.so va être utilisé. Mais lorsque valhalla.so va utiliser foobar, c’est aussi le foobar de player.so qui sera utilisé (aïe). Dans le cas 2 c’est le même principe mais inversé. Les conséquences peuvent être très imprévisibles.

Si vous avez une application qui est liée à des dizaines de bibliothèques il faut espérer que tout le monde ait pris la peine de faire deux choses importantes:

  1. Utilisez toujours un espace de nom pour vos fonctions. Par exemple libvalhalla en utilise trois  (libvalhalla_, valhalla_ et vh_). Un espace de nom en C ce n’est rien d’autre qu’un nom identique que vous concaténez au début des noms. Par exemple nos foobar auraient pût se nommer libplayer_foobar et libvalhalla_foobar, ce qui aurait évité la collision.
  2. Déclarez toujours en static toutes les fonctions qui ne sont pas utilisées en dehors du fichier source. Une fonction static n’a pas besoin d’espace de nom, car elle n’est jamais exportée! Et faire ainsi permet d’aider le compilateur à effectuer de meilleurs optimisations.

Comment détecter et debugger les collisions

Une des solution c’est de compiler votre programme entièrement en static. Il doit donc être lié aux .a de toutes les bibliothèques. Dans ce cas de figure, une collision sera forcément détectée par le linker.

Pour debugger commencez par tout compiler en -O0 -g3, puis utilisez valgrind. Vous arriverez à remonter sur l’appel de fonction qui s’est fait de la libA à la libB. Vous pourriez voir le foobar de player.so appelé par valhalla.so.

En pratique…

Il y a de nombreux mois, j’ai eu des problèmes de ce type avec libVLC et libplayer, car les fonctions de getopt étaient exportées par libVLC bien que c’était uniquement pour son propre usage. Ça me provoquait des collisions avec le getopt que j’utilise dans libplayer-test. J’ai bien entendu reporté le problème qui a été corrigé.

J’ai aussi eu un cas avec GeeXboX et le projet GuPNP. Le développeur principal a aussi été prévenu mais a priori il s’en fiche (ce n’est pas moi qui l’a contacté mais un autre du team). Du coup ce n’est pas possible de lier en static si on utilise deux de ses libs car elles ont les même fonctions non-static pour traiter le XML. Et le pire c’est que le nom de ces fonctions n’a pas d’espace de nom très original. Notez qu’en dynamique il n’y a jamais de problème car heureusement dans les deux bibliothèques, les fonctions sont les mêmes.

Bref, en fouillant bien on doit trouver ce genre d’exemple un peu partout…

J’espère que ce poste vous sera utile pour vos propres développements.

Bon code et à bientôt!

Mathieu SCHROETER

h1

De "POSIX" à Windows

2010.02.19

Hello,

La sortie d’Enna au début janvier à réveiller des critiques de tous les genres. En principe (faut être honnête) elles ne m’intéressent pas spécialement. Tout d’abord je n’estime pas qu’il y ait de concurrence entre les logiciels libres. Beaucoup de projets s’inspirent d’autres projets et c’est normal. Et si quelqu’un désire une fonctionnalité spécifique il a plusieurs solutions. La première c’est d’utiliser le projet qui offre la fonctionnalité (non?). La seconde c’est de critiquer simplement le projet car une fonctionnalité évidente est absente. Se sont ces critiques là que j’ignore spécialement, car elles n’apportent rien.  Après vous avez des gens qui critiquent mais qui aident spontanément et ils sont toujours les bienvenue.

De "POSIX" à Windows

Une des critique facile est de dire qu’Enna ne fonctionne pas sous Windows et qu’XBMC par contre est multi-plateforme. Les gens qui le disent ont tendance à oublier (ou alors à ne pas du tout connaitre, même dans les grandes lignes) l’histoire d’XBMC. Et oui, à l’origine XBMC ne fonctionnait pas nativement sous Linux. Le port à pris du temps, et c’est le même problème quand il faut porter dans l’autre sens.

Pour en revenir à Enna, je n’ai aucun intérêt personnel à l’avoir sous Windows. Néanmoins il y a eu des progrès pour qu’un jour, Enna puisse fonctionner sous Windows. Pour quand? Je n’en sais rien et ça n’a aucune importance.

Concernant le titre, si j’ai mis POSIX entre guillemets c’est parce que tout n’est pas vraiment du POSIX. Certaines choses sont des extensions du GNU par exemple. Il y a des adaptations à faire aussi entre les systèmes qui se basent sur POSIX. Même entre les noyaux Linux et *BSD, voir même Hurd.

Par exemple libvalhalla fonctionne correctement sous les noyaux Linux et FreeBSD (je pense spécialement à Debian GNU/kFreeBSD), elle fonctionne aussi avec Hurd (testé avec Debian GNU/Hurd) à la différence que les priorités sur les threads ne sont pas gérées correctement. Chaque noyau à sa façon de faire des threads et ça demande de prendre en compte les cas particuliers.

J’ai volontairement omis de mentionner Mac OS X, ou plus précisément Darwin. Bien qu’Apple dit qu’il soit POSIX-compliant, il y a qu’en même au moins un cas particulier dans libvalhalla car ce n’est pas si POSIX que ça.

MinGW

Le meilleur moyen de réaliser des ports Windows est sans aucun doute MinGW. C’est une base GCC et le compilateur peut être natif Windows ou alors compilé pour une compilation croisée sous GNU/Linux (ou d’autres OS). En principe depuis GNU/Linux on peut cross-compiler aussi bien pour Windows que pour Darwin (c’est ainsi que les différentes versions du générateur d’ISO sont faites). Néanmoins, ça peut paraitre étonnant mais il est plus facile de créer un compilateur croisé pour Windows (merci au projet MinGW) que pour Darwin.

J’ai deux cross-compilateurs binaires pour Darwin8 (PPC et i686). Ils ont été créés il y a maintenant plusieurs années par un ancien membre de GeeXboX. Malheureusement il est parti avec les secrets de fabrication. Je n’ai jamais réussi à les reproduire depuis les sources (et ce n’est pas faute d’avoir essayé). Si quelqu’un à des pistes, elles m’intéressent grandement!

libgeexbox-win32

Avant d’espérer Enna sous Windows il faut bien sûr se concentrer sur les dépendances. Et ce qui nous intéresse ici c’est donc libnfo, libplayer et libvalhalla. Au moment où j’écris cet article, libnfo et libvalhalla sont "complètement" supportés sous Windows. Je vais reprendre quelques éléments intéressants qui ont posés des problèmes.

Notez les guillemets, car en ce qui concerne libvalhalla il reste un potentiel problème. Mais néanmoins la bibliothèque est utilisable.

Libvalhalla utilise des temporisations à différents endroits. Celles-ci sont réalisées à l’aide de variable-conditions/mutex. L’idée est d’avoir des temporisations interruptibles contrairement à des fonctions du type sleep(), usleep() ou nanosleep() (attention, je parle bien de temporisations interruptibles sans l’aide de signaux). La bibliothèque Pthreads de POSIX offre tout ce dont on a besoin. Ainsi libvalhalla et libplayer reposent complètement sur celle-ci. Mais ce n’est pas directement de Pthreads que je désire parler, mais du temps pour pouvoir espérer avoir des temporisations plus ou moins précises. Les fonctions pthreads utilisent la structure `struct timespec` qui en théorie offre un champ à la nanoseconde. Même si la valeur peut être juste au moment de la lecture de l’horloge, les appels de fonctions prennent de toutes façon des nanosecondes/microsecondes. Et même pour un système temps réel dur, c’est très difficile de jouer dans ces ordres de grandeurs. Les seuls applications pratiques où je me suis vraiment amusé à compter les nanosecondes c’est lorsque que je faisais du VHDL sur un bon vieux Xilinx.

Bref.. passons.. Mon but est de pouvoir traiter des temporisations de plusieurs centaines de millisecondes. Ce qui est très facile avec un noyau Linux. La structure timespec évoquée précédemment se présente ainsi.

struct timespec {
  time_t sec;
  long int nsec;
}

Sous *BSD, Linux et Darwin il est très facile de la peupler. Concernant Mac OS X et son pseudo POSIX-compliant, le noyau Mach permet de récupérer une structure relativement semblable avec également des nanosecondes, mais la fonction POSIX clock_gettime() n’existe pas chez Apple. Que le champ nsec soit juste ou non ça n’a pas d’importance, pour autant qu’il ne soit pas faux dans les millisecondes. Finalement ces trois noyaux offrent les fonctions nécessaires et même plus. Mais on ne peut pas en dire autant de l’API Windows.

Une question de temps

GetSystemTime

Windows met à disposition des fonctions nommées GetSystemTime() et GetSystemTimeAsFileTime(). Elles sont sensées retourner une résolution à la milliseconde, respectivement à la centaine de nanoseconde. GetSystemTimeAsFileTime() est connu comme étant plus rapide que GetSystemTime(). Par contre cette fonction n’existe pas sous Windows CE et perd donc de son intérêt (dès le moment qu’on recherche la portabilité).

Voyez plutôt le résultat en pratique avec mon PC.

 WinXP                         GNU/Linux/Wine
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
GetSystemTime ()
 1266227465.000000000          1266233962.000000000
 1266227465.000000000          1266233962.000000000
 1266227465.000000000          1266233962.000000000
 1266227465.000000000          1266233962.000000000
 1266227465.000000000          1266233962.000000000
 - wait 1 ms
 1266227465.015000000          1266233962.001000000
 - wait 2 ms
 1266227465.031000000          1266233962.003000000
 - wait 3 ms
 1266227465.046000000          1266233962.006000000
 - wait 4 ms
 1266227465.062000000          1266233962.010000000
 - wait 5 ms
 1266227465.078000000          1266233962.015000000

A gauche il y a donc les résultats directement depuis Windows XP. A droite c’est le même programme mais exécuté à travers Wine (le même PC est utilisé). Les attentes de 1 à 5 ms sont réalisées simplement par la fonction Sleep() également mise à disposition par l’API Windows. Il est intéressant de noter que Windows n’arrive pas à descendre à la milliseconde avec un Sleep(1). Problème connu ceci dit…

A noter également que la fonction GetSystemTime() n’est pas des plus performante. Elle est reconnue comme étant peu propice à offrir réellement 1 ms de résolution. J’ai fais ainsi une seconde mesure avec 10’000 lectures du compteur, pour détecter la résolution effective.

Après plus de 8’000 lectures, Windows retourne vraiment 15 ms de plus que la lecture précédente.

 1266484241.000000000
 1266484241.000000000
 ... ~8000 fois ...
 1266484241.000000000
 1266484241.000000000
 1266484241.015000000
 1266484241.015000000
 1266484241.015000000
 1266484241.015000000

GetSystemTimeAsFileTime

J’ai donc refais les mêmes mesures mais avec GetSystemTimeAsFileTime() pour voir si on arrive à des meilleurs résultats. Le MSDN parle de 100 ns, on peut donc espérer une résolution utilisable à la milliseconde.

 WinXP                         GNU/Linux/Wine
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
GetSystemTimeAsFileTime ()
 1266227465.078125000          1266233962.015939000
 1266227465.078125000          1266233962.015941000
 1266227465.078125000          1266233962.015942000
 1266227465.078125000          1266233962.015944000
 1266227465.078125000          1266233962.015945000
 - wait 1 ms
 1266227465.093750000          1266233962.017012000
 - wait 2 ms
 1266227465.109375000          1266233962.019076000
 - wait 3 ms
 1266227465.125000000          1266233962.022139000
 - wait 4 ms
 1266227465.140625000          1266233962.026204000
 - wait 5 ms
 1266227465.156250000          1266233962.031268000

La première chose qui frappe ici, c’est que Windows semble donner que des valeurs multiples de 25. Donc d’une résolution de 25 us. On est encore relativement loin des 100 ns promis par le MSDN. Mais pour tester la vrai résolution, j’ai également fais tourner la lecture 10’000 fois.

On constate que Wine arrive à atteindre la microseconde. Néanmoins on n’a pas non plus la résolution de 100 ns. La raison est que Wine se base sur la fonction gettimeofday() qui sous les systèmes POSIX, ne donne pas une résolution meilleure que la microseconde. La structure est un timeval au lieu d’un timespec avec un champ usec au lieu de nsec.

Ici aussi, après environ 8’000 lectures, on constate une résolution d’exactement 109.375-93.75=15.625\,ms. C’est aussi mauvais qu’avant. Les microsecondes n’apportent absolument rien. Au début je me suis fais avoir car je pensais vraiment que les 25 us étaient atteints. Et bien que la fonction est sensée être plus rapide d’après mes recherches, en pratique (sous Windows XP), il n’y a pas de quoi en faire une montagne. Il a fallut presque le même nombre de lecture (un peu plus de 8000) pour environ 15 ms.

 1266484241.093750000
 1266484241.093750000
 ... ~8000 fois ...
 1266484241.093750000
 1266484241.093750000
 1266484241.109375000
 1266484241.109375000
 1266484241.109375000
 1266484241.109375000

Je pense qu’elle est considérée comme plus rapide car elle ne peuple pas une structure relativement complexe comme GetSystemTime (voir SYSTEMTIME). La structure utilisée avec la seconde fonction est FILETIME.

Finalement, comme première conclusion et pour garder la compatibilité avec Windows CE on peut utiliser GetSystemTime() sans regret.

clock_gettime

Il existe donc un moyen d’avoir une bien meilleur résolution. Le principe est d’utiliser l’horloge haute résolution (la TSC dans les processeurs x86) afin d’atteindre la nanoseconde. Pour ce faire, Windows met à disposition deux fonctions, QueryPerformanceFrequeny() conjointement avec QueryPerformanceCounter().

Le but final est de simuler la fonction clock_gettime() de POSIX qui permet d’atteindre une résolution de 1 ns.

La première fonction donne la fréquence de l’horloge haute résolution et la seconde donne le nombre de ticks depuis la mise en route. La fréquence donnée est toujours (à peu de chose près) un multiple de 1193182 Hz.

Le principe est donc de retrouver le tick qui correspond à un temps précis depuis EPOCH. Puis de retrouver le temps en divisant simplement le nombre de ticks par la fréquence. L’horloge étant au minimum cadencée à 1193182 Hz, on devrait avoir au moins une résolution de \frac{1}{1193182}=838.10\,ns.

 WinXP                         GNU/Linux/Wine
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 Freq: 3579545                 Freq: 1193182
 1266227465.156298915          1266233962.031381633
 1266227465.156301150          1266233962.031384147
 1266227465.156302826          1266233962.031385823
 1266227465.156304502          1266233962.031387499
 1266227465.156306178          1266233962.031389176
 - wait 1 ms
 1266227465.171900059          1266233962.032571728
 - wait 2 ms
 1266227465.187522995          1266233962.034622547
 - wait 3 ms
 1266227465.203157943          1266233962.037689975
 - wait 4 ms
 1266227465.218764954          1266233962.041754736
 - wait 5 ms
 1266227465.234392918          1266233962.046825211

A noter que Wine donne toujours la fréquence la plus basse. Cette fréquence normalement dépend du matériel, mais Wine se base sur Linux pour récupérer le temps. Ainsi la fréquence peut être arbitraire. Windows XP me donne par contre une fréquence pas tout à fait correcte. Comme je l’ai dis avant, celle-ci devrait être un multiple de 1193182, pourtant pour que ce multiple soit vrai, il faudrait alors 3579546 au lieu de 3579545. Je suppose que la fonction QueryPerformanceFrequeny() n’arrondit pas la valeur.

On trouve ici un pas de 1676/1677 ns. Aussi bien avec Windows qu’avec Wine. Ce qui est très bon. Le temps perdu vient désormais des appels de fonctions et non plus de l’imprécision des valeurs de temps.

On pourrait crier victoire, mais en réalité il y a encore un problème potentiel. L’horloge haute résolution est indépendante. Ce qui veut dire qu’elle va forcément diverger par rapport à l’horloge qui donne le "vrai" temps. Ainsi sur une longue période, l’erreur entre les deux va s’agrandir linéairement.

Étant donné que la résolution de GetSystemTimeAsFileTime() est trop imprécise pour de courtes mesures, il est nécessaire de faire des mesures sur plusieurs heures pour avoir des résultats significatifs.  Seul la résolution de 15.625 ms peut servir de référence et une telle divergence ne peut pas être détectée sur quelques minutes (à moins que QueryPerformanceCounter() et QueryPerformanceFrequeny() soient complètement faux). Je n’ai donc rien à vous montrer au sujet de ce potentiel problème de divergence.

La synchronisation

Bien que je ne connaisse pas encore la divergence entre les horloges, on peu légitimement se poser la question de la resynchronisation.

Le principe est d’utiliser une information qui est fiable. Et donc a priori c’est la seconde. L’idée est de récupérer le tick qui correspond au changement de seconde. Ensuite ce tick est converti en un temps depuis EPOCH (un temps absolu en seconde). On mémorise cette seconde pour toute la durée de vie du programme.

Dès que clock_gettime() est appelé, on regarde la valeur du compteur de l’horloge haute résolution, puis on la soustrait à la valeur qui correspond aux secondes du début. On a donc une différence de valeur du compteur. On la divise par la fréquence du compteur ce qui nous donne la différence de temps. On additionne ce nouveau temps avec les secondes du départ pour enfin avoir le temps en nanoseconde depuis EPOCH.

Le potentiel problème avec la synchronisation vient spécialement du fait d’utiliser la seconde comme référence. Si la synchronisation commence au début d’une nouvelle seconde, il faut attendre quasiment une seconde pour terminer la synchronisation. Ainsi actuellement dans le libvalhalla pour Windows, il n’y a pas de resynchronisation. En fonction du décalage entre l’horloge haute résolution et l’horloge du temps, les timers finissent par se rentrer dedans ou alors par devenir de plus en plus écartés. Le fait qu’ils divergent ou convergent dépend du matériel.

Je vois deux solutions pour le moment.

  1. Faire la resynchronisation en parallèle au reste du programme. Ainsi on peut continuer d’utiliser clock_gettime() avec la précédente synchronisation.
  2. Synchroniser sur GetSystemTimeAsFileTime() avec son pas de 15.625 ms. Mais la compatibilité avec Windows CE est perdue.

Il reste aussi à déterminer quand est-ce qu’il faut resynchroniser.

Un autre problème vient des changements de l’heure du système. Si cela arrive, actuellement libvalhalla aura toutes les temporisations faussées sous Windows.

Les Pthreads

Finalement, on peut se demander si tout cela vaut la peine. Les Pthreads pour Windows ont été conçus pour fonctionner sur un maximum de versions de Windows. Ainsi la référence de temps utilisée se fait via GetSystemTime(). Le clock_gettime() utilisé dans libvalhalla à deux raisons d’être. D’abord il sert à donner un temps absolu aux fonctions Pthreads, et il sert à faire les mesures de temps pour les statistiques. L’aberration dans tout ce travail sur un clock_gettime() pour Windows est simplement que le temps donné aux fonctions Pthreads est de bien meilleur résolution que la résolution du temps interne au Pthreads-win32  (il faudrait néanmoins que je vérifie ce point, je n’ai fais que survoler les sources de Pthreads-win32). Et avoir une résolution à la nanoseconde pour des statistiques n’apporte rien.

Un des seul intérêt restant c’est donc le petit défi que ça représente.

J’hésite à enlever tout le code relatif à QueryPerformanceCounter() pour n’utiliser que GetSystemTime() avec sa misérable résolution. Ou alors rajouter un test sur la fonction GetSystemTimeAsFileTime() pour la préférer à GetSystemTime() si elle existe. Tout ces problèmes me rappel toujours un peu plus pourquoi Windows à un noyau  qui n’a rien de plus que les autres. Mais qui au contraire, ne créer que des problèmes supplémentaires.

Speedhack

Je profite de cet article pour présenter les speedhacks (ces logiciels de triches permettant par exemple de se déplacer plus vite dans un jeu, très prisé à l’époque sur Counter-Strike).

Si j’en parle ici c’est qu’ils reposent sur les fonctions de l’horloge haute résolution, et plus précisément QueryPerformanceCounter(). Il y a un peu plus d’un an, j’avais écris un article à ce sujet que vous pouvez lire à cette adresse. J’en ai profité pour y faire deux trois améliorations et corrections.

A bientôt,

Mathieu SCHROETER

Suivre

Recevez les nouvelles publications par mail.

%d bloggers like this: