Fréquence du processeur
Un article de Haypo.
Retour aux articles de programmation
La première version de cet article a été publiée par Victor Stinner le 29 Mars 2003. Une ancienne version est disponible sur developpez.com
Mise à jour (4 fév. 2006) : correction du code source donné en exemple, la fonction usleep de Linux utilise des µs et pas des ms, merci rebergue ;-)
Sommaire |
[modifier] Introduction
Je vais tenter de vous expliquer comment lire la fréquence du processeur, accéder à l'instruction RDTSC (Read Timestamp Counter), et créer un chroomètre ultra-précis sous Linux ou Windows. Ce code ne fonctionne que sur les processeurs compatibles Intel Pentium à cause de l'instruction RDTSC. Ne vous inquiétez pas votre (et mon) AMD Athlon la supporte parfaitement ;-)
Ne perdez pas votre temps à copier/coller le code source, un code complet en C est donné à la fin de cette page.
Les extraits de codes pour Linux fonctionnent avec GCC, les extraits pour Windows fonctionnent avec Borland C++ Builder. Le code à télécharger fonctionne également sous Visual C++. Ce dernier nécessite des bouts de code pour gérer les nombres en 64 bits, ce qui complique l'article.
Si vous avez des suggestions et des questions : contactez-moi !
[modifier] Lecture de la fréquence du CPU sous Linux
[modifier] Fichier /proc/cpuinfo
Sous Linux, c'est facile car le noyau crée un fichier (/proc/cpuinfo) contenant la fréquence du processeur. La ligne utile est "cpu MHz : <fréquence>".
Exemple de fichier cpuinfo :
processor : 0 vendor_id : GenuineIntel cpu family : 6 model : 7 model name : Pentium III (Katmai) stepping : 3 cpu MHz : 451.030 cache size : 512 KB fdiv_bug : no hlt_bug : no f00f_bug : no coma_bug : no fpu : yes fpu_exception : yes cpuid level : 2 wp : yes flags : fpu vme de pse tsc msr pae mce cx8 sep mtrr pge mca cmov pat pse36 mmx fxsr sse bogomips : 897.84
On voit ici que c'est un processeur Intel Pentium III ayant une fréquence de 450 Mhz (la fréquence calculée est de 451.030 MHz).
Le texte ("cpu MHz") et la valeur sont séparés par des espaces et/ou des tabulations, un double point (":"), puis un espace ou une tabulation.
[modifier] Code source
Ce qui va nous donner le code suivant :
#define NOMFICH_CPUINFO "/proc/cpuinfo"
// Lit la fréquence du processeur
// Renvoie 0 en cas d'échec
int LitFrequenceCpu (double* frequence)
{
const char* prefixe_cpu_mhz = "cpu MHz";
FILE* F;
char ligne[300+1];
char *pos;
int ok=0;
// Ouvre le fichier
F = fopen(NOMFICH_CPUINFO, "r");
if (!F) return 0;
// Lit une ligne apres l'autre
while (!feof(F))
{
// Lit une ligne de texte
fgets (ligne, sizeof(ligne), F);
// C'est la ligne contenant la frequence?
if (!strncmp(ligne, prefixe_cpu_mhz, strlen(prefixe_cpu_mhz)))
{
// Oui, alors lit la frequence
pos = strrchr (ligne, ':') +2;
if (!pos) break;
if (pos[strlen(pos)-1] == '\n') pos[strlen(pos)-1] = '\0';
strcpy (ligne, pos);
strcat (ligne,"e6");
*frequence = atof (ligne);
ok = 1;
break;
}
}
fclose (F);
return ok;
}
J'ai choisi le format double, car il fonctionne avec tous les compilateurs et a une précision suffisante.
[modifier] Lire la fréquence du CPU sous Windows
[modifier] Compteur QueryPerformance
Sous Windows, c'est plus complexe : il faut utiliser l'instruction RDTSC des processeurs compatibles Intel Pentium (sur les autres processeur, je ne sais pas comment faire), et les fonctions QueryPerformanceFrequency et QueryPerformanceCounter.
C'est un problème physique assez simple : on a deux compteurs qu'on va appeler WIN (compteur Windows) et CPU (compteur processeur RDTSC). On sait mesure une durée avec les deux compteurs :
- Twin est la différence de temps du compteur WIN, mesurée avec deux appels à QueryPerformanceCounter.
- Tcpu est la différence de temps du compteur CPU, mesurée avec deux appels à RDTSC.
Par contre, on ne connait que la fréquence du compteur WIN, donnée par la fonction QueryPerformanceFrequency, qu'on va noter Fwin. Pour rappel, une fréquence est un nombre d'impulsions exprimé en Hertz (Hz) ou "par seconde" (s^-1) . On notera la fréquence du compteur CPU : Fcpu.
Donc, pour une pause d'exactement Texact secondes, on a donc les équations :
- Texact =Twin / Fwin
- Texact = Tcpu / Fcpu
- => Twin / Fwin = Tcpu / Fcpu
- Enfin : Fcpu = (Tcpu * Fwin ) / Twin
On notera que la durée de la pause Texact n'entre plus en compte dans le calcul ... en théorie. Mais en pratique, plus la pause est longue meilleure est la précision. J'ai retenu Texact = 500 ms, car c'est le meilleur compromis entre précision et durée de la pause (= blocage du programme).
Dans le code donné à la fin du programme, j'utilise encore une autre technique : j'attend une certaine durée en lisant sans arrêt le chronomètre Windows. Ceci permet d'avoir des pauses très courtes, tout en gardant une précision correcte.
[modifier] QueryPerformance et fréquence variable
Il ne faut plus utiliser, avec les Pentium 4, Pentium M Dual core, et Core 2 Duo et supérieurs (uniquement chez Intel) le compteur QueryPerformanceCounter. En effet, celui-ci est valable uniquement si la fréquence du processeur est constante (au coefficient près) depuis le lancement de Windows.
Si l'utilisateur modifie la fréquence sous Windows (ex: avec ClockGen), QueryPerformanceCounter ne sera pas mis à jour. Le temps sera encore donc le temps calculé à partir du démarrage de Windows.
Pour cela, il faut utiliser TimeGetTime qui n'est pas touché par ce soucis. Cette fonction a une précision de l'ordre du millième de seconde (spécifié via TimeBegin). C'est le deuxième compteur le plus précis de Windows.
Les informations de cette section m'ont été envoyée par Charles VIL, merci à lui.
[modifier] Code source
Ce qui donne le code :
// Lit la fréquence du processeur
// Renvoie la fréquence en Hz dans 'frequence' si le code de retour est
// différent de 1. Renvoie 0 en cas d'erreur.
int LitFrequenceCpu (double* frequence)
{
unsigned __int64 Fwin;
unsigned __int64 Twin_avant, Twin_apres;
double Tcpu_avant, Tcpu_apres;
double Fcpu;
// Lit la frequence du chronometre Windows
if (!QueryPerformanceFrequency((LARGE_INTEGER*)&Fwin)) return 0;
printf ("Frequence du compteur Windows = ");
AfficheFrequence (Fwin);
// Avant
Tcpu_avant = RDTSC();
QueryPerformanceCounter((LARGE_INTEGER*)&Twin_avant);
// Pause de 500 ms
Sleep(500);
// Apres
Tcpu_apres = RDTSC();
QueryPerformanceCounter((LARGE_INTEGER*)&Twin_apres);
// Calcule la fréquence en MHz
Fcpu = (Tcpu_apres - Tcpu_avant);
Fcpu *= Fwin;
Fcpu /= (double)(Twin_apres - Twin_avant);
*frequence = Fcpu;
return 1;
}
Même remarque que pour le code Linux : J'ai choisi le format double, car il fonctionne avec tous les compilateurs et a une précision suffisante.
[modifier] L'instruction RDTSC
Les processeurs compatibles Intel Pentium possède une instructeur permettant de lire le nombre de cycles processseurs depuis l'allumage de l'ordinateur. C'est donc un nombre non signé sur 64 bits que l'on va lire. Il nous faut donc une variable pouvant stocker un tel nombre. Plusieurs solutions s'offrent à nous :
- Utiliser le format "unsigned long long" disponible avec la norme ISO C99. Mais peu de compilateurs la supporte. Le seul que je connais est GCC (http://gcc.gnu.org).
- Emuler un nombre 64 bits en stocker deux nombres de 32 bits (au format "unsigned long"). Ceci fonctionne la totalité des compilateurs C (ou bien ?).
- Utiliser le format "double"qui a une taille et une précision suffisante.
L'instruction en question est "RDTSC" (Read Timestamp Counter), ou en code machine "db 0x0F, 0x31".
Ce qui nous donne le code suivant :
// Instruction RDTSC du processeur Pentium
double RDTSC(void)
{
#ifdef linux
unsignedlonglong x;
__asm__ volatile (".byte 0x0f, 0x31" : "=A"(x));
return (double)x;
#else
unsignedlong a, b;
double x;
asm
{
db 0x0F,0x31
mov [a],eax
mov [b],edx
}
x = b;
x *= 4294967296;
x += a;
return x;
#endif
}
Ce code fonctionne à la fois sous Linux et sous Windows (grâce à l'utilisation du "#ifdef linux").
[modifier] Chronomètre ultra-précis
Maintenant que nous avons les deux outils nécessaires : fréquence du processeur et instruction RDTSC, on peut lire une durée très précisément. Pour cela, il faut :
- Lire la fréquence du processeur frequence (format double)
- Sauver le compteur RDTSC dans une variable avant (format double)
- Faire les calculs ...
- Sauver le compteur RDTSC dans une variable apres (format double)
- Calculer la différence de temps duree = (apres - avant) / frequence
Voilà pour l'algorithme, en pratique : voyez le code source donné plus bas :-)
[modifier] Code source en C
Plutôt que de copier/coller inutilement, je vous propose de charger un exemple complet qui fonctionne simultanément sous Linux et sous Windows.
Télécharger le code C complet (6 Ko)
Attention, le code est sous licence GPL !
Ce code source fonctionne sous Linux avec GCC 2.95, Borland C++ Builder 5 (Windows) et Visual C++ 6 (Windows). Bien sûr, il doit être compatible avec d'autres compilateurs moyennant une petite correction du code.
[modifier] Code source en assembleur
Jean-Paul MICHEL m'a envoyé un code source en assembleur pour lire la fréquence du processeur en assembleur. Il l'offre gratos, trop sympa ! Par contre, je vous prierai de respecter le copyright !!!
Télécharger le code assembleur
[modifier] Processeurs à fréquence variable
Il faut pouvoir disposer d'un accès en ring0 pour l'application, ensuite il faut lire (pour les Intel P4, Pentium M, Core 2 Duo et supérieurs) le registre MSR 0x198 qui renvoie, sur quelques bits du registre eax, le coefficient actuel du processeur, et sur quelques bits du registre edx le coefficient maximal du processeur.
Pour pouvoir obtenir la bonne fréquence du processeur, il faut donc: calculer RDTSC, ensuite multiplier par le ratio (sur la fréquence obtenue) : coefficient_Actuel/coefficient_Maximal sachant que le coefficient maximal est invariant, seul le coefficient actuel peut changer. Ainsi, RDTSC qui est depuis ces processeurs invariant, devient bon et renvoi la bonne valeur...
Les AMD Phenom (et équivalents) sont touchés par le même soucis, mais disposent d'un bit de sureté permettant d'indiquer si oui ou non le rdtsc est synchronisé avec la fréquence, il s'agit du bit 24 de eax sur le MSR 0xC0010015. Il faut toujours s'assurer que ce bit est correctement activé pour ne pas avoir de problème de lecture (pour toujours avoir le TSC synchrone à la fréquence), s'il n'est pas bien activé il suffit de le réécrire cela ne pose pas de soucis !
Les informations de cette section m'ont été envoyée par Charles VIL, merci à lui.

