2012-09-28 11 views
50

Supponiamo di provare a utilizzare il tsc per il monitoraggio delle prestazioni e vogliamo evitare il riordino delle istruzioni.Differenza tra rdtscp, rdtsc: memoria e cpuid/rdtsc?

Queste sono le nostre opzioni:

1:rdtscp è una chiamata serializzazione. Impedisce il riordino della chiamata a rdtscp.

__asm__ __volatile__("rdtscp; "   // serializing read of tsc 
        "shl $32,%%rdx; " // shift higher 32 bits stored in rdx up 
        "or %%rdx,%%rax" // and or onto rax 
        : "=a"(tsc)  // output to tsc variable 
        : 
        : "%rcx", "%rdx"); // rcx and rdx are clobbered 

Tuttavia, rdtscp è disponibile solo su CPU più recenti. Quindi in questo caso dobbiamo usare rdtsc. Ma rdtsc non è serializzato, quindi usarlo da solo non impedirà alla CPU di riordinarlo.

modo che possiamo utilizzare una di queste due opzioni per evitare riordino:

2: Questa è una chiamata alla cpuid e poi rdtsc. cpuid è una chiamata di serializzazione.

volatile int dont_remove __attribute__((unused)); // volatile to stop optimizing 
unsigned tmp; 
__cpuid(0, tmp, tmp, tmp, tmp);     // cpuid is a serialising call 
dont_remove = tmp;        // prevent optimizing out cpuid 

__asm__ __volatile__("rdtsc; "   // read of tsc 
        "shl $32,%%rdx; " // shift higher 32 bits stored in rdx up 
        "or %%rdx,%%rax" // and or onto rax 
        : "=a"(tsc)  // output to tsc 
        : 
        : "%rcx", "%rdx"); // rcx and rdx are clobbered 

3: Questa è una chiamata a rdtsc con memory nella lista clobber, che impedisce riordino

__asm__ __volatile__("rdtsc; "   // read of tsc 
        "shl $32,%%rdx; " // shift higher 32 bits stored in rdx up 
        "or %%rdx,%%rax" // and or onto rax 
        : "=a"(tsc)  // output to tsc 
        : 
        : "%rcx", "%rdx", "memory"); // rcx and rdx are clobbered 
                // memory to prevent reordering 

mia comprensione per il 3 ° opzione è la seguente:

Rendere il chiamare __volatile__ impedisce all'ottimizzatore di rimuovere asm o spostarlo attraverso qualsiasi istruzione che potrebbe richiedere i risultati (o modificare gli input) di asm. Tuttavia potrebbe ancora spostarlo rispetto alle operazioni non correlate. Quindi __volatile__ non è sufficiente.

Comunica che la memoria del compilatore è stata danneggiata: : "memory"). Il clobber "memory" significa che GCC non può fare supposizioni sul fatto che il contenuto della memoria rimanga lo stesso su ASM, e quindi non lo riordina attorno ad esso.

Quindi le mie domande sono:

  • 1: E 'la mia comprensione della __volatile__ e "memory" corretta?
  • 2: le seconde due chiamate fanno la stessa cosa?
  • 3: L'utilizzo di "memory" sembra molto più semplice dell'utilizzo di un'altra istruzione di serializzazione. Perché qualcuno dovrebbe usare la terza opzione sulla seconda opzione?
+9

Sembra confondere il riordino delle istruzioni generate dal compilatore, che è possibile evitare usando 'volatile' e' memory' e il riordino delle istruzioni eseguite dal processore (noto anche come _out of order execution_), che si evita usando ' cpuid'. – hirschhornsalz

+0

@hirschhornsalz ma non avrà 'memoria' nell'elenco dei clobber per impedire al processore di riordinare le istruzioni? La 'memoria' non si comporta come una barriera di memoria? –

+0

o forse la 'memoria' nell'elenco dei clobber viene solo emessa in gcc e il codice macchina risultante non lo espone al processore? –

risposta

35

Come menzionato in un commento, c'è una differenza tra una barriera compilatore ed una barriera processore . volatile e memory nella dichiarazione ASM fungono da barriera del compilatore, ma il processore è ancora libero di riordinare le istruzioni.

La barriera del processore è istruzioni speciali che devono essere espressamente fornite, ad es. rdtscp, cpuid, istruzioni della barriera di memoria (mfence, lfence, ...) ecc.

Per inciso, mentre usando cpuid come barriera prima rdtsc è comune, può anche essere molto male dal punto di vista delle prestazioni, in quanto piattaforme di macchine virtuali spesso trappola e emulare l'istruzione cpuid per imporre un insieme comune di CPU funzionalità su più macchine in un cluster (per garantire che la migrazione live funzioni). Quindi è meglio usare una delle istruzioni della recinzione di memoria.

Il kernel Linux utilizza mfence;rdtsc su piattaforme AMD e su Intel. Se non vuoi preoccuparti di distinguere tra questi, mfence;rdtsc funziona su entrambi, anche se è leggermente più lento come mfence è una barriera più forte di lfence.

+5

'cpuid; rdtsc' non riguarda le recinzioni di memoria, si tratta di serializzare il flusso di istruzioni. Di solito è usato per scopi di benchmarking per assicurarsi che nessuna "vecchia" istruzione rimanga nella memoria di riordino/stazione di prenotazione. Il tempo di esecuzione di 'cpuid' (che è piuttosto lungo, ricordo> 200 cicli) deve essere quindi sottratto. Se il risultato è più "esatto", in questo modo non mi è del tutto chiaro, ho sperimentato con e senza e le differenze sembrano essere meno l'errore naturale di misurazione, anche in modalità utente singolo senza nient'altro. – hirschhornsalz

+0

Non sono sicuro, ma forse l'istruzione di recinzione utilizzata in questo modo nel kernel non è affatto utile ^^ – hirschhornsalz

+4

@hirschhornsalz: in base ai log di commit git, AMD e Intel hanno confermato che m/lfence serializzerà rdtsc al momento CPU disponibili. Suppongo che Andi Kleen possa fornire maggiori dettagli su cosa è stato detto esattamente, se sei interessato e chiedi a lui. – janneb

5

è possibile utilizzarlo come indicato qui sotto:

asm volatile (
"CPUID\n\t"/*serialize*/ 
"RDTSC\n\t"/*read the clock*/ 
"mov %%edx, %0\n\t" 
"mov %%eax, %1\n\t": "=r" (cycles_high), "=r" 
(cycles_low):: "%rax", "%rbx", "%rcx", "%rdx"); 
/* 
Call the function to benchmark 
*/ 
asm volatile (
"RDTSCP\n\t"/*read the clock*/ 
"mov %%edx, %0\n\t" 
"mov %%eax, %1\n\t" 
"CPUID\n\t": "=r" (cycles_high1), "=r" 
(cycles_low1):: "%rax", "%rbx", "%rcx", "%rdx"); 

Nel codice di cui sopra, la prima chiamata CPUID implementa una barriera per evitare l'esecuzione out-of-order delle istruzioni sopra e sotto le istruzioni RDTSC. Con questo metodo evitiamo di chiamare un'istruzione CPUID tra le letture dei registri in tempo reale

Il primo RDTSC legge quindi il registro timestamp e il valore è memorizzato nella memoria . Quindi viene eseguito il codice che vogliamo misurare. L'istruzione RDTSCP legge il registro timestamp per la seconda volta e garantisce che l'esecuzione di tutto il codice che volevamo misurare sia completata. Le due istruzioni "mov" in seguito memorizzano i valori dei registri edx e eax in memoria. Infine, una chiamata CPUID garantisce che una barriera venga implementata di nuovo in modo tale che sia impossibile che qualsiasi istruzione venga successivamente eseguita prima di CPUID stessa.

+12

Ciao, sembra che abbiate copiato questa risposta dal white paper di Gabriele Paolinis "Come confrontare i tempi di esecuzione del codice su Intel® IA-32 e IA-64 Instruction Set Architectures" (avete saltato una interruzione di linea). Stai usando il lavoro di qualcun altro senza dare credito all'autore. Perché non aggiungere un'attribuzione? –

+0

Sì, davvero, è affrontato. Mi chiedo anche se i due movimenti in lettura dell'ora di inizio siano necessari: http://stackoverflow.com/questions/38994549/is-intels-timestamp-reading-asm-code-example-using-two-more-registers -than-are –

+0

C'è una ragione specifica per avere due variabili alto e basso? – ExOfDe