Skywalker13

Diary of an ex-GeeXboX developer…

De POSIX à Windows ~ 🇫🇷

Posted at — Feb 19, 2010

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 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 1’193’182 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 Ă  1’193’182 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 1’193’182, pourtant pour que ce multiple soit vrai, il faudrait alors 3’579’546 au lieu de 3’579’545. Je suppose que la fonction QueryPerformanceFrequeny() n’arrondit pas la valeur.

On trouve ici un pas de 16761677 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.

Etant 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