web-dev-qa-db-fra.com

Comment obtenir le nombre de cycles CPU dans x86_64 à partir de C ++?

J'ai vu ce post sur SO qui contient du code C pour obtenir le dernier nombre de cycles CPU:

profilage basé sur le nombre de cycles CPU dans C/C++ Linux x86_64

Existe-t-il un moyen d'utiliser ce code en C++ (les solutions Windows et Linux sont les bienvenues)? Bien qu'écrit en C (et C étant un sous-ensemble de C++), je ne suis pas trop certain si ce code fonctionnerait dans un projet C++ et sinon, comment le traduire?

J'utilise x86-64

EDIT2:

Trouvé cette fonction mais ne peut pas obtenir VS2010 pour reconnaître l'assembleur. Dois-je inclure quelque chose? (Je crois que je dois échanger uint64_t à long long Pour les fenêtres....?)

static inline uint64_t get_cycles()
{
  uint64_t t;
  __asm volatile ("rdtsc" : "=A"(t));
  return t;
}

EDIT3:

Du code ci-dessus, j'obtiens l'erreur:

"erreur C2400: erreur de syntaxe de l'assembleur en ligne dans 'opcode';" type de données "trouvé" "

Quelqu'un pourrait-il m'aider?

26
user997112

À partir de GCC 4.5 et versions ultérieures, la __rdtsc() intrinsèque est désormais prise en charge par MSVC et GCC.

Mais l'inclusion nécessaire est différente:

#ifdef _WIN32
#include <intrin.h>
#else
#include <x86intrin.h>
#endif

Voici la réponse originale avant GCC 4.5.

Tiré directement de l'un de mes projets:

#include <stdint.h>

//  Windows
#ifdef _WIN32

#include <intrin.h>
uint64_t rdtsc(){
    return __rdtsc();
}

//  Linux/GCC
#else

uint64_t rdtsc(){
    unsigned int lo,hi;
    __asm__ __volatile__ ("rdtsc" : "=a" (lo), "=d" (hi));
    return ((uint64_t)hi << 32) | lo;
}

#endif

Ceci GNU C Extended asm indique au compilateur:

  • volatile: les sorties ne sont pas une pure fonction des entrées (il faut donc les réexécuter à chaque fois, pas réutiliser un ancien résultat).
  • "=a"(lo) et "=d"(hi): les opérandes de sortie sont des registres fixes: EAX et EDX. ( contraintes machine x86 ). L'instruction x86 rdtsc place son résultat 64 bits dans EDX: EAX, donc laisser le compilateur choisir une sortie avec "=r" Ne fonctionnerait pas: il n'y a aucun moyen de demander au CPU le résultat pour aller ailleurs.
  • ((uint64_t)hi << 32) | lo - étendez zéro les deux moitiés 32 bits à 64 bits (car lo et hi sont unsigned), et déplacez-les logiquement + OR ensemble en un seul 64 -bit C variable. Dans le code 32 bits, ce n'est qu'une réinterprétation; les valeurs restent toujours dans une paire de registres 32 bits. En code 64 bits, vous obtenez généralement un décalage réel + OR asm instructions, à moins que la moitié supérieure ne s'optimise.

(NDLR: cela pourrait probablement être plus efficace si vous utilisiez unsigned long au lieu de unsigned int. Le compilateur saurait alors que lo était déjà étendu à zéro dans RAX. Il ne Je ne sais pas que la moitié supérieure était nulle, donc | et + sont équivalents s’ils voulaient fusionner différemment. L’intrinsèque devrait en théorie vous offrir le meilleur des deux mondes dans la mesure où laisser l'optimiseur faire du bon travail.)

https://gcc.gnu.org/wiki/DontUseInlineAsm si vous pouvez l'éviter. Mais j'espère que cette section est utile si vous avez besoin de comprendre l'ancien code qui utilise asm en ligne afin que vous puissiez le réécrire avec intrinsèques. Voir aussi https://stackoverflow.com/tags/inline-Assembly/info

51
Mysticial

Votre asm en ligne est cassé pour x86-64. "=A" En mode 64 bits permet au compilateur de choisir soit RAX ou RDX, pas EDX: EAX. Voir ce Q & A pour plus


Vous n'avez pas besoin d'asm en ligne pour cela . Il n'y a aucun avantage; les compilateurs ont des fonctions intégrées pour rdtsc et rdtscp, et (au moins ces jours-ci) définissent tous un __rdtsc intrinsèque si vous incluez les bons en-têtes. Mais contrairement à presque tous les autres cas ( https://gcc.gnu.org/wiki/DontUseInlineAsm ), il n'y a pas d'inconvénient sérieux à asm, tant que vous utilisez un bon et mise en œuvre sûre comme @ Mysticial.

Malheureusement, MSVC n'est pas d'accord avec tout le monde sur l'en-tête à utiliser pour les intrinsèques non SIMD.

le guide des intrinisques d'Intel dit que _rdtsc (Avec un trait de soulignement) est dans <immintrin.h>, Mais cela ne fonctionne pas sur gcc et clang. Ils ne définissent que les intrinsèques SIMD dans <immintrin.h>, Donc nous sommes coincés avec <intrin.h> (MSVC) contre <x86intrin.h> (Tout le reste, y compris ICC récent). Pour la compatibilité avec MSVC et la documentation d'Intel, gcc et clang définissent les versions à un et deux traits de soulignement de la fonction.

Fait amusant: la version à double soulignement renvoie un entier 64 bits non signé, tandis qu'Intel documente _rdtsc() comme renvoyant (signé) __int64.

// valid C99 and C++

#include <stdint.h>  // <cstdint> is preferred in C++, but stdint.h works.

#ifdef _MSC_VER
# include <intrin.h>
#else
# include <x86intrin.h>
#endif

// optional wrapper if you don't want to just use __rdtsc() everywhere
inline
uint64_t readTSC() {
    // _mm_lfence();  // optionally wait for earlier insns to retire before reading the clock
    uint64_t tsc = __rdtsc();
    // _mm_lfence();  // optionally block later instructions until rdtsc retires
    return tsc;
}

// requires a Nehalem or newer CPU.  Not Core2 or earlier.  IDK when AMD added it.
inline
uint64_t readTSCp() {
    unsigned dummy;
    return __rdtscp(&dummy);  // waits for earlier insns to retire, but allows later to start
}

Compile avec les 4 principaux compilateurs: gcc/clang/ICC/MSVC, pour 32 ou 64 bits. Voir les résultats sur l'explorateur du compilateur Godbolt , y compris quelques appelants de test.

Ces intrinsèques étaient nouvelles dans gcc4.5 (à partir de 2010) et clang3.5 (à partir de 2014) . gcc4.4 et clang 3.4 sur Godbolt ne compilent pas cela, mais gcc4.5.3 (avril 2011) le fait. Vous pouvez voir asm en ligne dans l'ancien code, mais vous pouvez et devez le remplacer par __rdtsc(). Les compilateurs de plus d'une décennie produisent généralement du code plus lent que gcc6, gcc7 ou gcc8 et ont des messages d'erreur moins utiles.

Le MSVC intrinsèque a (je pense) existé bien plus longtemps, car MSVC n'a jamais supporté asm en ligne pour x86-64. ICC13 a __rdtsc Dans immintrin.h, Mais n'a pas du tout x86intrin.h. Les ICC plus récents ont x86intrin.h, Du moins la façon dont Godbolt les installe pour Linux.

Vous voudrez peut-être les définir comme signés long long, surtout si vous voulez les soustraire et les convertir en float. int64_t -> float/double est plus efficace que uint64_t Sur x86 sans AVX512. De plus, de petits résultats négatifs pourraient être possibles en raison des migrations de CPU si les TSC ne sont pas parfaitement synchronisés, et cela a probablement plus de sens que d'énormes nombres non signés.


BTW, clang a également une __builtin_readcyclecounter() portable qui fonctionne sur n'importe quelle architecture. (Retourne toujours zéro sur les architectures sans compteur de cycle.) Voir les documents d'extension de langage clang/LLVM


Pour en savoir plus sur en utilisant lfence (ou cpuid) pour améliorer la répétabilité de rdtsc et contrôler exactement quelles instructions sont/ne sont pas dans le intervalle chronométré en bloquant l'exécution dans le désordre , voir la réponse de @HadiBrais sur clflush pour invalider la ligne de cache via la fonction C et les commentaires pour un exemple de la différence cela fait.

Voir aussi LFENCE sérialise-t-il sur les processeurs AMD? (TL: DR oui avec l'atténuation Spectre activée, sinon les noyaux laissent le MSR pertinent non défini donc vous devez utiliser cpuid pour sérialiser.) Cela a toujours été défini comme une sérialisation partielle sur Intel.

Comment comparer les temps d'exécution de code sur les architectures de jeux d'instructions Intel® IA-32 et IA-64, un livre blanc Intel à partir de 2010.


rdtsc count reference cycles, pas les cycles d'horloge du processeur

Il compte à une fréquence fixe indépendamment du turbo/économie d'énergie, donc si vous voulez une analyse uops-by-clock, utilisez des compteurs de performance. rdtsc est exactement corrélé avec l'heure de l'horloge murale (sauf pour les ajustements d'horloge du système, c'est donc une source de temps parfaite pour steady_clock). Il coche à la fréquence nominale du processeur, c'est-à-dire à la fréquence indiquée sur l'autocollant. (Ou presque ça. par exemple. 2592 MHz sur un Skylake i7-6700HQ 2,6 GHz.)

Si vous l'utilisez pour le micro-benchmarking, incluez d'abord une période d'échauffement pour vous assurer que votre CPU est déjà à la vitesse d'horloge maximale avant de commencer le chronométrage. (Et désactivez éventuellement le turbo et dites à votre système d'exploitation de préférer la vitesse d'horloge maximale pour éviter les décalages de fréquence du processeur pendant votre microbenchmark). Ou mieux, utilisez une bibliothèque qui vous donne accès à des compteurs de performances matérielles, ou une astuce comme statistique de performance pour une partie du programme si votre région chronométrée est suffisamment longue pour pouvoir attacher un perf stat -p PID .

Cependant, vous voudrez toujours garder l'horloge du processeur fixe pour les microbenchmarks, à moins que vous ne vouliez voir comment différentes charges feront que Skylake s'arrêtera lorsqu'il est lié à la mémoire ou autre. (Notez que la bande passante/latence de la mémoire est généralement fixe, en utilisant une horloge différente de celle des cœurs. À vitesse d'horloge inactive, un échec de cache L2 ou L3 prend beaucoup moins de cycles d'horloge de base.)

  • Mesures de cycle d'horloge négatives avec rdtsc dos à dos? l'histoire de RDTSC: à l'origine les CPU ne faisaient pas d'économie d'énergie, donc le TSC était à la fois des horloges en temps réel et des horloges centrales. Ensuite, il a évolué à travers diverses étapes à peine utiles dans sa forme actuelle de source de temps utile à faible surcharge découplée des cycles d'horloge de base (constant_tsc), Qui ne s'arrête pas lorsque l'horloge s'arrête (nonstop_tsc ). Aussi quelques conseils, par exemple ne prenez pas le temps moyen, prenez la médiane (il y aura des valeurs aberrantes très élevées).
  • std :: chrono :: horloge, horloge matérielle et nombre de cycles
  • Obtenir des cycles de CPU en utilisant RDTSC - pourquoi la valeur de RDTSC augmente-t-elle toujours?
  • Cycles perdus sur Intel? Une incohérence entre rdtsc et CPU_CLK_UNHALTED.REF_TSC
  • mesure des temps d'exécution de code en C à l'aide de l'instruction RDTSC répertorie certains pièges, y compris SMI (interruptions de gestion du système) que vous ne pouvez pas éviter même en mode noyau avec cli), et virtualisation de rdtsc sous une VM. Et bien sûr, des choses de base comme des interruptions régulières étant possibles, alors répétez votre timing plusieurs fois et jetez les valeurs aberrantes.
  • Déterminer la fréquence TSC sous Linux . L'interrogation programmée de la fréquence TSC est difficile et peut-être pas possible, en particulier dans l'espace utilisateur, ou peut donner un résultat pire que de l'étalonner . L'étalonnage à l'aide d'une autre source de temps connue prend du temps. Consultez cette question pour en savoir plus sur la difficulté de convertir TSC en nanosecondes (et ce serait bien si vous pouviez demander au système d'exploitation quel est le taux de conversion, car le système d'exploitation l'a déjà fait au démarrage).

    Si vous effectuez un micro-benchmarking avec RDTSC à des fins de réglage, votre meilleur pari est simplement d'utiliser des ticks et de sauter même en essayant de convertir en nanosecondes. Sinon, utilisez un fonction de temps de bibliothèque haute résolution comme std::chrono ou clock_gettime. Voir équivalent plus rapide de gettimeofday pour une discussion/comparaison des fonctions d'horodatage, ou lire un horodatage partagé de la mémoire pour éviter rdtsc entièrement si votre exigence de précision est suffisamment faible pour une interruption ou un thread de temporisation pour le mettre à jour.

    Voir aussi Calculer le temps système à l'aide de rdtsc sur la recherche de la fréquence et du multiplicateur du cristal.

Il n'est pas non plus garanti que les TSC de tous les cœurs soient synchronisés . Donc, si votre thread migre vers un autre cœur de processeur entre __rdtsc(), il peut y avoir un biais supplémentaire. (La plupart des systèmes d'exploitation tentent de synchroniser les TSC de tous les cœurs, cependant, ils seront donc normalement très proches.) Si vous utilisez rdtsc directement, vous souhaiterez probablement épingler votre programme ou votre thread à un noyau, par exemple avec taskset -c 0 ./myprogram sous Linux.

opération de récupération du CPU TSC, en particulier dans un environnement multicœur multiprocesseur indique que Nehalem et les versions plus récentes ont synchronisé et verrouillé le TSC pour tous les cœurs d'un package (ie TSC invariant). Mais les systèmes multiprises peuvent toujours être un problème. Même les systèmes plus anciens (comme avant Core2 en 2007) peuvent avoir un TSC qui s'arrête lorsque l'horloge principale s'arrête, ou qui est lié à la fréquence d'horloge principale réelle au lieu des cycles de référence. (Les CPU plus récents ont toujours un TSC constant et un TSC non-stop.) Voir la réponse de @ amdn à cette question pour plus de détails.


Quelle est la qualité de l'utilisation de l'intrinsèque?

C'est à peu près aussi bon que ce que vous obtiendriez de @ Mysticial's GNU C inline asm, ou mieux car il sait que les bits supérieurs de RAX sont mis à zéro. La principale raison pour laquelle vous voudriez garder asm en ligne est pour compat avec de vieux compilateurs croustillants.

Une version non en ligne de la fonction readTSC elle-même se compile avec MSVC pour x86-64 comme ceci:

unsigned __int64 readTSC(void) PROC                             ; readTSC
    rdtsc
    shl     rdx, 32                             ; 00000020H
    or      rax, rdx
    ret     0
  ; return in RAX

Pour les conventions d'appel 32 bits qui renvoient des entiers 64 bits dans edx:eax, C'est juste rdtsc/ret. Ce n'est pas important, vous voulez toujours que cela soit en ligne.

Dans un appelant de test qui l'utilise deux fois et soustrait pour chronométrer un intervalle:

uint64_t time_something() {
    uint64_t start = readTSC();
    // even when empty, back-to-back __rdtsc() don't optimize away
    return readTSC() - start;
}

Les 4 compilateurs font un code assez similaire. Il s'agit de la sortie 32 bits de GCC:

# gcc8.2 -O3 -m32
time_something():
    Push    ebx               # save a call-preserved reg: 32-bit only has 3 scratch regs
    rdtsc
    mov     ecx, eax
    mov     ebx, edx          # start in ebx:ecx
      # timed region (empty)

    rdtsc
    sub     eax, ecx
    sbb     edx, ebx          # edx:eax -= ebx:ecx

    pop     ebx
    ret                       # return value in edx:eax

Il s'agit de la sortie x86-64 de MSVC (avec démêlage de nom appliqué). gcc/clang/ICC émettent tous un code identique.

# MSVC 19  2017  -Ox
unsigned __int64 time_something(void) PROC                            ; time_something
    rdtsc
    shl     rdx, 32                  ; high <<= 32
    or      rax, rdx
    mov     rcx, rax                 ; missed optimization: lea rcx, [rdx+rax]
                                     ; rcx = start
     ;; timed region (empty)

    rdtsc
    shl     rdx, 32
    or      rax, rdx                 ; rax = end

    sub     rax, rcx                 ; end -= start
    ret     0
unsigned __int64 time_something(void) ENDP                            ; time_something

Les 4 compilateurs utilisent or + mov au lieu de lea pour combiner les moitiés basse et haute dans un registre différent. Je suppose que c'est une sorte de séquence en conserve qu'ils ne parviennent pas à optimiser.

Mais écrire un changement/lea inline asm vous-même n'est guère mieux. Vous priveriez le compilateur de la possibilité d'ignorer les 32 bits élevés du résultat dans EDX, si vous chronométrez un intervalle si court que vous ne conservez qu'un résultat 32 bits. Ou si le compilateur décide de stocker l'heure de début dans la mémoire, il pourrait simplement utiliser deux magasins 32 bits au lieu de shift/ou/mov. Si 1 uop supplémentaire dans le cadre de votre timing vous dérange, vous feriez mieux d'écrire votre microbenchmark entier en pure asm.

Cependant, nous pouvons peut-être obtenir le meilleur des deux mondes avec une version modifiée du code de @ Mysticial:

// More efficient than __rdtsc() in some case, but maybe worse in others
uint64_t rdtsc(){
    // long and uintptr_t are 32-bit on the x32 ABI (32-bit pointers in 64-bit mode), so #ifdef would be better if we care about this trick there.

    unsigned long lo,hi;  // let the compiler know that zero-extension to 64 bits isn't required
    __asm__ __volatile__ ("rdtsc" : "=a" (lo), "=d" (hi));
    return ((uint64_t)hi << 32) + lo;
    // + allows LEA or ADD instead of OR
}

Sur Godbolt , cela donne parfois un meilleur asm que __rdtsc() pour gcc/clang/ICC, mais d'autres fois, il incite les compilateurs à utiliser un registre supplémentaire pour enregistrer lo et hi séparément, donc clang peut être optimisé en ((end_hi-start_hi)<<32) + (end_lo-start_lo). Espérons que s'il y a une réelle pression de registre, les compilateurs se combineront plus tôt. (gcc et ICC enregistrent toujours lo/hi séparément, mais n'optimisent pas aussi bien.)

Mais gcc8 32 bits en fait un gâchis, en compilant même juste la fonction rdtsc() elle-même avec un réel add/adc Avec des zéros au lieu de simplement retourner le résultat dans edx: eax comme clang. (gcc6 et les versions antérieures fonctionnent bien avec | au lieu de +, mais préférez certainement la __rdtsc() intrinsèque si vous vous souciez du code-gen 32 bits de gcc).

19
Peter Cordes

VC++ utilise une syntaxe entièrement différente pour l'assemblage en ligne - mais uniquement dans les versions 32 bits. Le compilateur 64 bits ne prend pas du tout en charge l'assemblage en ligne.

Dans ce cas, c'est probablement aussi bien - rdtsc a (au moins) deux problèmes majeurs en ce qui concerne les séquences de code de synchronisation. Tout d'abord (comme la plupart des instructions), il peut être exécuté dans le désordre, donc si vous essayez de chronométrer une courte séquence de code, le rdtsc avant et après ce code peut être exécuté avant, ou les deux après ou ce que vous avez (je suis à peu près sûr que les deux s'exécuteront toujours dans l'ordre l'un par rapport à l'autre, donc au moins la différence ne sera jamais négative).

Deuxièmement, sur un système multicœur (ou multiprocesseur), un rdtsc peut s'exécuter sur un cœur/processeur et l'autre sur un cœur/processeur différent. Dans un tel cas, un résultat négatif est tout à fait possible.

D'une manière générale, si vous voulez une minuterie précise sous Windows, vous ferez mieux d'utiliser QueryPerformanceCounter.

Si vous insistez vraiment sur l'utilisation de rdtsc, je pense que vous devrez le faire dans un module séparé écrit entièrement en langage Assembly (ou utiliser un compilateur intrinsèque), puis lié à votre C ou C++. Je n'ai jamais écrit ce code pour le mode 64 bits, mais en mode 32 bits, il ressemble à ceci:

   xor eax, eax
   cpuid
   xor eax, eax
   cpuid
   xor eax, eax
   cpuid
   rdtsc
   ; save eax, edx

   ; code you're going to time goes here

   xor eax, eax
   cpuid
   rdtsc

Je sais que cela semble étrange, mais c'est en fait juste. Vous exécutez CPUID car il s'agit d'une instruction de sérialisation (ne peut pas être exécutée dans le désordre) et est disponible en mode utilisateur. Vous l'exécutez trois fois avant de commencer le chronométrage, car Intel documente le fait que la première exécution peut/se déroulera à une vitesse différente de la seconde (et ce qu'ils recommandent est trois, donc trois c'est le cas).

Ensuite, vous exécutez votre code en cours de test, un autre cpuid pour forcer la sérialisation et le rdtsc final pour obtenir l'heure après la fin du code.

Parallèlement à cela, vous souhaitez utiliser tous les moyens fournis par votre système d'exploitation pour forcer tout cela à s'exécuter sur un processus/noyau. Dans la plupart des cas, vous souhaitez également forcer l'alignement du code - les changements d'alignement peuvent entraîner des différences assez importantes dans le taux d'exécution.

Enfin, vous voulez l'exécuter un certain nombre de fois - et il est toujours possible qu'il soit interrompu au milieu des choses (par exemple, un changement de tâche), vous devez donc être préparé à la possibilité d'une exécution prenant un peu plus long que le reste - par exemple, 5 courses qui prennent environ 40 à 43 cycles d'horloge chacun, et une sixième qui prend plus de 10000 cycles d'horloge. De toute évidence, dans ce dernier cas, vous jetez simplement la valeur aberrante - ce n'est pas de votre code.

Résumé: réussir à exécuter l'instruction rdtsc elle-même est (presque) le moindre de vos soucis. Il y a un peu plus que vous besoin à faire avant d'obtenir des résultats de rdtsc qui signifieront réellement n'importe quoi.

7
Jerry Coffin

Pour Windows, Visual Studio fournit un "compilateur intrinsèque" pratique (c'est-à-dire une fonction spéciale, que le compilateur comprend) qui exécute l'instruction RDTSC pour vous et vous donne le résultat:

unsigned __int64 __rdtsc(void);
5
Nik Bougalis