Ottieni il conteggio del ciclo della CPU?

Ho visto questo post su SO che contiene il codice C per ottenere il più recente conteggio del ciclo CPU:

Profiling basato su CPU Cycle basato su C / C ++ Linux x86_64

C’è un modo in cui posso usare questo codice in C ++ (le soluzioni windows e linux sono benvenute)? Sebbene scritto in C (e C essendo un sottoinsieme di C ++) non sono sicuro se questo codice funzionerebbe in un progetto C ++ e, in caso contrario, come tradurlo?

Sto usando x86-64

EDIT2:

Trovato questa funzione ma non può ottenere che VS2010 riconosca l’assemblatore. Devo includere qualcosa? (Credo di dover uint64_t da uint64_t a long long per windows ….?)

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

Edit3:

Dal codice precedente ho ricevuto l’errore:

“errore C2400: errore di syntax dell’assemblatore inline in ‘opcode’; trovato ‘tipo di dati'”

Qualcuno potrebbe aiutarmi?

A partire da GCC 4.5 e __rdtsc() successive, l’ __rdtsc() intrinseco è ora supportato da MSVC e GCC.

Ma l’inclusione necessaria è diversa:

 #ifdef _WIN32 #include  #else #include  #endif 

Ecco la risposta originale prima di GCC 4.5.

Estratto direttamente da uno dei miei progetti:

 #include  // Windows #ifdef _WIN32 #include  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 

VC ++ utilizza una syntax completamente diversa per l’assembly inline, ma solo nelle versioni a 32 bit. Il compilatore a 64 bit non supporta affatto l’assembly inline.

In questo caso, probabilmente è altrettanto positivo – rdtsc ha (almeno) due problemi principali quando si tratta di sequenze di codice di temporizzazione. Per prima cosa (come la maggior parte delle istruzioni) può essere eseguito fuori ordine, quindi se stai provando a rdtsc una breve sequenza di codice, l’ rdtsc prima e dopo quel codice potrebbe essere eseguito prima di esso, o entrambi dopo di esso, o cosa hanno tu (sono abbastanza sicuro che i due saranno sempre eseguiti in ordine l’uno rispetto all’altro, quindi almeno la differenza non sarà mai negativa).

In secondo luogo, su un sistema multi-core (o multiprocessore), un rdtsc poteva essere eseguito su un core / processore e l’altro su un core / processore differente. In tal caso, un risultato negativo è interamente ansible.

In generale, se vuoi un timer preciso sotto Windows, starai meglio usando QueryPerformanceCounter .

Se davvero insisti nell’usare rdtsc , credo che dovrai farlo in un modulo separato scritto interamente in linguaggio assembly (o usare un compilatore intrinseco), quindi collegato al tuo C o C ++. Non ho mai scritto quel codice per la modalità a 64 bit, ma nella modalità a 32 bit sembra qualcosa del genere:

  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 

So che questo sembra strano, ma in realtà è giusto. Esegui CPUID perché è un’istruzione serializzante (non può essere eseguita fuori servizio) ed è disponibile in modalità utente. Lo si esegue tre volte prima di iniziare il cronometraggio perché Intel documenta il fatto che la prima esecuzione può / verrà eseguita a una velocità diversa dalla seconda (e ciò che raccomandano è tre, quindi tre è).

Quindi esegui il codice sotto test, un’altra cpuid per forzare la serializzazione e il rdtsc finale per ottenere il tempo dopo il completamento del codice.

Insieme a ciò, si desidera utilizzare qualsiasi mezzo che il sistema operativo fornisce per forzare tutto ciò su un unico processo / core. Nella maggior parte dei casi, si desidera forzare anche l’allineamento del codice: le modifiche nell’allineamento possono portare a differenze abbastanza sostanziali in termini di spee di esecuzione.

Infine, si desidera eseguirlo più volte – ed è sempre ansible che venga interrotto nel mezzo di alcune cose (ad esempio un interruttore di attività), quindi è necessario essere preparati alla possibilità di un’esecuzione che richiede un po ‘di tempo. più lungo del resto – ad esempio, 5 corse che richiedono ~ 40-43 cicli di clock a testa, e un sesto che richiede più di 100 cicli di clock. Chiaramente, in quest’ultimo caso, butti fuori l’outlier – non è dal tuo codice.

Riepilogo: riuscire a eseguire l’istruzione rdtsc stessa è (quasi) l’ultima delle tue preoccupazioni. C’è ancora un po ‘di più che devi fare prima di poter ottenere risultati da rdtsc che in realtà significhi qualcosa.

Per Windows, Visual Studio fornisce un comodo “compilatore intrinseco” (ovvero una funzione speciale, che il compilatore comprende) che esegue l’istruzione RDTSC per te e ti restituisce il risultato:

 unsigned __int64 __rdtsc(void); 

Non hai bisogno di asm in linea per questo . Non c’è beneficio; i compilatori hanno built-in per rdtsc e rdtscp , e (almeno in questi giorni) definiscono un __rdtsc intrinsico se si includono gli header giusti. Ma a differenza di quasi tutti gli altri casi ( https://gcc.gnu.org/wiki/DontUseInlineAsm ), non c’è un serio svantaggio di asm, a patto che tu stia usando un’implementazione buona e sicura come @ Mysticial , non uno con una rottura "=A" vincolo .

Sfortunatamente MSVC non è d’accordo con tutti gli altri su quale intestazione usare per intrinseca non SIMD.

La guida di Intel sugli intrinchi dice che _rdtsc (con un carattere di sottolineatura) si trova in , ma che non funziona su gcc e clang. Definiscono solo intrinseche SIMD in , quindi siamo bloccati con (MSVC) e (tutto il resto, incluso il recente ICC). Per compat con MSVC e la documentazione di Intel, gcc e clang definiscono sia le versioni a un trattino singolo sia quelle a due trattini bassi della funzione.

Fatto divertente: la versione double-underscore restituisce un intero senza segno a 64 bit, mentre Intel documenta _rdtsc() come _rdtsc() (firmato) __int64 .

 // valid C99 and C++ #include  //  is preferred in C++, but stdint.h works. #ifdef _MSC_VER # include  #else # include  #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 } 

Compilare con tutti e 4 i principali compilatori: gcc / clang / ICC / MSVC, per 32 o 64 bit. Vedi i risultati sul explorer del compilatore Godbolt , inclusi alcuni caller di test.

Queste intrinseche erano nuove in gcc4.5 (dal 2010) e clang3.5 (dal 2014) . gcc4.4 e clang 3.4 su Godbolt non lo compilano, ma gcc4.5.3 (aprile 2011) lo fa. Potresti vedere in linea asm nel vecchio codice, ma puoi e dovresti sostituirlo con __rdtsc() . I compilatori più vecchi di un decennio di solito producono codice più lento di gcc6, gcc7 o gcc8 e hanno meno messaggi di errore utili.

L’intrinseco MSVC ha (credo) esistito molto più a lungo, perché MSVC non ha mai supportato in linea asm per x86-64. ICC13 ha __rdtsc in immintrin.h , ma non ha affatto un x86intrin.h . ICC più recente ha x86intrin.h , almeno il modo in cui Godbolt li installa per Linux.

Potresti definirli come firmati long long , specialmente se vuoi sottrarli e convertirli in float. int64_t -> float / double è più efficiente di uint64_t su x86 senza AVX512. Inoltre, piccoli risultati negativi potrebbero essere possibili a causa delle migrazioni della CPU se i TSC non sono perfettamente sincronizzati, e questo probabilmente ha più senso dei grandi numeri senza segno.


BTW, clang ha anche un __builtin_readcyclecounter() portatile che funziona su qualsiasi architettura. (Restituisce sempre zero su architetture senza contatore di cicli.) Vedere i documenti di estensione della lingua clang / LLVM


Per ulteriori informazioni sull’uso di lfence (o cpuid ) per migliorare la ripetibilità di rdtsc e controllare esattamente quali istruzioni sono / non sono nell’intervallo di tempo bloccando l’esecuzione fuori ordine , vedere la risposta di @HadiBrais su clflush per invalidare la linea di cache tramite C funzione e i commenti per un esempio della differenza che fa.

Vedi anche LFENCE serializza su processori AMD? (TL: DR sì con mitigazione di cpuid abilitato, altrimenti i kernel lasciano il MSR unset rilevante, quindi dovresti usare cpuid per serializzare.) È sempre stato definito come serializzazione parziale su Intel.

Come confrontare i tempi di esecuzione del codice su Intel® IA-32 e IA-64 Instruction Set Architectures , un white paper Intel del 2010.


rdtsc conta i cicli di riferimento , non i cicli di clock del core della CPU

Conta a una frequenza fissa indipendentemente dal turbo / risparmio energetico, quindi se si desidera un’analisi UOP per ora, utilizzare i contatori delle prestazioni. rdtsc è esattamente correlato al tempo di wall-clock (ad eccezione delle regolazioni dell’orologio di sistema, quindi è steady_clock perfetto per steady_clock ). Segna alla frequenza stimata della CPU, cioè la frequenza della vignetta pubblicizzata. (O quasi questo, ad es. 2592 MHz su Skylake i7-6700HQ a 2,6 GHz.)

Se lo si utilizza per il microbenchmarking, includere innanzitutto un periodo di riscaldamento per assicurarsi che la CPU sia già alla massima velocità di clock prima di iniziare a cronometrare. (E, facoltativamente, disabilita il turbo e comunica al tuo sistema operativo di preferire la massima velocità di clock per evitare spostamenti di frequenza della CPU durante il tuo microbenchmark). O meglio, usa una libreria che ti dà accesso ai contatori delle prestazioni dell’hardware, o un trucco come perf stat per parte del programma se la tua regione temporizzata è abbastanza lunga da poter colbind una perf stat -p PID .

Di solito, vorrete comunque mantenere l’orologio della CPU fissato per i microbenchmark, a meno che non vogliate vedere come carichi diversi faranno svanire Skylake quando è legato alla memoria o altro. (Si noti che la larghezza di banda / latenza della memoria è in gran parte fissa, utilizzando un clock diverso rispetto ai core. Alla velocità di inattività, una perdita della cache L2 o L3 richiede molti meno cicli di clock core.)

  • Misure del ciclo di orologio negativo con rdtsc back-to-back? la storia di RDTSC: in origine le CPU non eseguivano il risparmio energetico, quindi il TSC era sia in tempo reale che nei core clock. Quindi si è evoluto attraverso vari passaggi a malapena utili nella sua forma attuale di un’utile nonstop_tsc basso overhead disaccoppiata dai cicli di clock core ( constant_tsc ), che non si interrompe quando l’orologio si ferma ( nonstop_tsc ). Anche alcuni suggerimenti, ad esempio, non prendere il tempo medio, prendere la mediana (ci saranno valori anomali molto alti).
  • std :: chrono :: clock, orologio hardware e numero di cicli
  • Ottenere cicli cpu usando RDTSC – perché il valore di RDTSC aumenta sempre?
  • Cicli persi su Intel? Incoerenza tra rdtsc e CPU_CLK_UNHALTED.REF_TSC
  • misurare i tempi di esecuzione del codice in C usando le istruzioni RDTSC elenca alcuni trucchi, incluso SMI (interrupt di gestione del sistema) che non è ansible evitare anche in modalità kernel con cli ), e la virtualizzazione di rdtsc sotto una VM. E, naturalmente, cose di base come interruzioni regolari sono possibili, quindi ripeti i tempi molte volte e butta via i valori anomali.
  • Determina la frequenza TSC su Linux . L’interrogazione programmatica della frequenza TSC è difficile e forse non ansible, specialmente nello spazio utente, o può dare risultati peggiori rispetto alla calibrazione . La calibrazione con un’altra sorgente temporale conosciuta richiede tempo. Vedere questa domanda per ulteriori informazioni su quanto sia difficile convertire TSC in nanosecondi (e sarebbe bello se si potesse chiedere al sistema operativo quale sia il rapporto di conversione, perché il sistema operativo lo ha già fatto all’avvio).

    Se stai facendo un microbenchmarking con RDTSC per scopi di tuning, la soluzione migliore è usare semplicemente tick e saltare anche cercando di convertire in nanosecondi. Altrimenti, usa una funzione di tempo di libreria ad alta risoluzione come std::chrono o clock_gettime . Vedere l’ equivalente più veloce di gettimeofday per alcune discussioni / confronti delle funzioni di timestamp, o la lettura di un timestamp condiviso dalla memoria per evitare rdtsc interamente se il tuo requisito di precisione è sufficientemente basso per un interrupt del timer o un thread per aggiornarlo.

    Vedi anche Calcola il tempo di sistema usando rdtsc per trovare la frequenza del cristallo e il moltiplicatore.

Inoltre, non è garantito che i TSC di tutti i core siano sincronizzati . Quindi, se il tuo thread passa a un altro core della CPU tra __rdtsc() , può esserci un eccesso di inclinazione. (La maggior parte dei sistemi operativi tenta di sincronizzare i TSC di tutti i core, quindi, normalmente, saranno molto vicini.) Se stai usando rdtsc direttamente, probabilmente vuoi appuntare il tuo programma o thread su un core, ad esempio con taskset -c 0 ./myprogram su Linux.

L’operazione di recupero TSC della CPU, specialmente nell’ambiente multicore-multiprocessore, dice che Nehalem e più recente hanno il TSC sincronizzato e bloccato insieme per tutti i core in un pacchetto (cioè TSC invariante). Ma i sistemi multi-socket possono ancora essere un problema. Anche i sistemi più vecchi (come prima di Core2 nel 2007) potrebbero avere un TSC che si arresta quando si interrompe l’orologio principale, o che è legato alla frequenza effettiva dell’orologio principale invece dei cicli di riferimento. (Le CPU più recenti hanno sempre TSC costante e TSC non-stop.) Vedi la risposta di @ amdn su quella domanda per maggiori dettagli.


Quanto è buono l’asm dall’usare l’intrinseco?

È buono come quello che si ottiene da @ Mysticial GNU C in linea asm, o meglio perché sa che i bit superiori di RAX sono azzerati. Il motivo principale per cui vuoi mantenere in linea asm è compatibile con vecchi compilatori croccanti.

Una versione non inline della funzione readTSC si compila da sola con MSVC per x86-64:

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

Per convenzioni di chiamata a 32 bit che restituiscono interi a 64 bit in edx:eax , è solo rdtsc / ret . Non è importante, lo vuoi sempre in linea.

In un chiamante di test che lo usa due volte e sottrae al tempo un intervallo:

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

Tutti e 4 i compilatori fanno un codice abbastanza simile. Questo è l’output a 32 bit di 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 

Questo è l’output x86-64 di MSVC (con applicazione del nome-demangling). gcc / clang / ICC emettono tutti codice identico.

 # 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 

Tutti e 4 i compilatori usano or + mov invece di lea per combinare le metà basse e quelle alte in un registro diverso. Immagino sia una specie di sequenza in scatola che non riescono a ottimizzare.

Ma scrivere uno shift / lea in linea asm da soli non è certo migliore. Privilegi il compilatore dell'opportunità di ignorare gli alti 32 bit del risultato in EDX, se stai calcolando un intervallo così breve da mantenere solo un risultato a 32 bit. O se il compilatore decide di memorizzare l'ora di inizio in memoria, potrebbe semplicemente usare due negozi a 32 bit invece di shift / o / mov. Se 1 extra-uop come parte del tuo tempismo ti infastidisce, è meglio scrivere il tuo intero microbenchmark in puro asm.

Tuttavia, possiamo ottenere il meglio da entrambi i mondi con una versione modificata del codice @ 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 } 

Su Godbolt , questo a volte dà meglio di __rdtsc() per gcc / clang / ICC, ma altre volte induce i compilatori a usare un registro extra per salvare lo e hi in modo separato, quindi clang può ottimizzare in ((end_hi-start_hi)<<32) + (end_lo-start_lo) . Speriamo che se c'è reale pressione del registro, i compilatori si uniranno prima. (gcc e ICC salvano ancora lo / hi separatamente, ma non ottimizzano anche.)

Ma gcc8 a 32 bit ne fa un casino, compilando anche solo la funzione rdtsc() stessa con un effettivo add/adc con zeri invece di restituire il risultato in edx: eax like clang fa. (gcc6 e precedenti vanno bene con | invece di + , ma preferisco decisamente l' __rdtsc() intrinseco se ti interessa il codice-gen a 32-bit da gcc).