octobre
2008
Suite à une question du forum.
A quoi sert timeBeginPeriod (API win32) et quelle est son influence sur le timing du VSync (dans Direct3D) ?
Pour résumer : l’OS Windows est un OS multitâche, c’est à dire qu’à un moment donné il y a N applications différentes qui peuvent s’exécuter : on va les appeler Processus (process en anglais) – même si théoriquement application et processus peuvent être différents -, et à l’intérieur de chaque processus il y a des fils d’executions différents (thread en anglais) qui s’exécutent en parallèle.
Comment l’OS fait-il cela ? Et bien il va diviser le temps d’execution processeur en tranches. Si application A et B tournent en même temps : il va donner la première tranche d’execution à A, puis la deuxième tranche à B etc. Si les tranches sont petites alors pour l’utilisateur c’est effectivement comme si A et B s’exécutaient en simultané. Les threads natifs et la gestion des process de l’OS est indépendant du nombre de cores du CPU, il y a peu la plupart des PC vendus avaient un CPU avec un seul core.
Le truc qui permet à cela de fonctionner c’est que le multitâche est invisible pour l’application, ce qui facilite la programmation d’applications mais complique l’OS qui doit empêcher une tâche de prendre plus de temps que la tranche allouée. Il fait ça en lançant des interruptions toutes les X nanosecondes (via un timer interne qui va générer des interruptions systèmes), lorsque l’interruption arrive, meme si l’application A n’a pas fini sa tache courante, le noyau de l’OS va arrêter son exécution et donner la main à l’application B.
Bien entendu ce changement est coûteux (relativement et en temps processeur, ça arrive tout de même des centaines de fois par secondes) : il faut sauver l’état de A (son contexte) pour que quand B a fini, A puisse reprendre comme si de rien n’était. Ce coût doit donc être amorti sur un grand nombre d’instructions pour pouvoir être négligé. Si on fixe chaque tranche à 10ms, cela donne un bon paquet d’instructions exécutables avant d’avoir à changer de contexte.
Seul problème, cette division en tranche de 10ms est plutôt bonne pour les applications génériques et les serveurs (ce pour quoi NT/2K étaient chargé à la base) mais elle est mauvaise pour toute application qui nécessite un timing très précis (temps réel, rendu vidéo etc). En effet une application qui passe la main, n’est pas sûre d’être rappelée avant N * 10ms, N le nombre d’applications qui attendent leur tour pour être exécutées. Si l’application n’a pas besoin de beaucoup de temps de calcul CPU (attente active) mais tu aimerais être prévenu d’un évènement avec un délai court (moins de x millisecondes) alors ce n’est pas idéal. Certaines personnes préconisent de faire une attente active à 100% (sans rendre la main aux autres processus). Là encore aucune garantie si la tranche courante est écoulée, l’OS va obliger à rendre la main. Par ailleurs l’utilisation du CPU devient non optimale là où on pourrait mettre à profit les mesures de réduction de consommation de courant (CPU idle). Ou l’on pourrait permettre à un autre thread de faire un traitement en tâche de fond pendant ce temps.
En échange quand on passe la taille de chaque tranche à 1ms à la place de 10ms (par exemple), on cause un process switch toutes les 1ms au minimum soit dix fois plus souvent, et le coût de l’échange peut donc être amorti sur beaucoup moins d’instructions qu’avant. D’où le coût CPU supplémentaire à charge apparemment équivalente. Au coût du switch il faut rajouter l’invalidation des caches du processeur (le process qui prend la main n’utilise probablement pas les mêmes données). La période minimale est de 1ms, mais on peut obtenir des résultats proches avec une période de 2ms avec un coût CPU moindre.
Au final, donc l’utilisation de timeBeginPeriod ne se justifie que si l’on a mis en évidence des problèmes de timing. De plus même avec une période petite, rien ne garantit le temps réel : l’OS garantit juste un « best effort » qui peut-être très loin de l’idéal. En pratique pour une application « temps réel » comme un jeu en mode exclusive (plein écran), usuellement la seule concurrence pour les cycles d’horloge est elle-même. Rien n’empêche également d’entamer la période courte juste avant une période d’attente active (le moment où l’on peut rendre la main mais on désire être réveillé rapidement), puis de restaurer la période à son niveau normal après l’attente active.