Differenza tra rdtscp, rdtsc: memoria e cpuid / rdtsc?

Supponiamo di provare a utilizzare il tsc per il monitoraggio delle prestazioni e vogliamo evitare il riordino delle istruzioni.

Queste sono le nostre opzioni:

1: rdtscp è una chiamata di 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 sulle nuove CPU. Quindi in questo caso dobbiamo usare rdtsc . Ma rdtsc non è serializzato, quindi usarlo da solo non impedirà alla CPU di riordinarlo.

Quindi possiamo usare una di queste due opzioni per impedire il riordino:

    2: Questa è una chiamata a cpuid e quindi a 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 dei clobber, che impedisce il 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 

    La mia comprensione per la terza opzione è la seguente:

    Effettuare la chiamata __volatile__ impedisce all’ottimizzatore di rimuovere asm o spostarlo su 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 è abbastanza.

    Dì al compilatore che la memoria è stata danneggiata : "memory") . Il clobber della "memory" significa che GCC non può fare ipotesi sul fatto che il contenuto della memoria resti lo stesso su tutto il dominio, e quindi non lo riordina attorno ad esso.

    Quindi le mie domande sono:

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

    Come accennato in un commento, c’è una differenza tra una barriera del compilatore e una barriera del processore . volatile e memory nell’istruzione asm agiscono da barriera del compilatore, ma il processore è ancora libero di riordinare le istruzioni.

    Le barriere del processore sono istruzioni speciali che devono essere fornite esplicitamente, ad esempio rdtscp, cpuid , istruzioni della fence di memoria ( mfence, lfence, …) ecc.

    Come parte, mentre si usa cpuid come barriera prima che rdtsc sia comune, può anche essere molto negativo dal punto di vista delle prestazioni, poiché le piattaforms di macchine virtuali spesso intercettano ed emulano le istruzioni della cpuid per imporre un insieme comune di funzioni della CPU 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 di Linux usa mfence;rdtsc su piattaforms AMD e lfence;rdtsc su Intel. Se non vuoi preoccuparti di distinguere tra questi, mfence;rdtsc funziona su entrambi, sebbene sia leggermente più lento dato che mfence è una barriera più forte di lfence .

    puoi usarlo come mostrato 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 sopra, la prima chiamata CPUID implementa una barriera per evitare l’esecuzione fuori servizio delle istruzioni sopra e sotto l’istruzione 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 viene memorizzato. 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 imansible che qualsiasi istruzione venga successivamente eseguita prima di CPUID stessa.