webentwicklung-frage-antwort-db.com.de

Unterschied zwischen rdtscp, rdtsc: memory und cpuid / rdtsc?

Angenommen, wir versuchen, den tsc für die Leistungsüberwachung zu verwenden, und wir möchten die Neuordnung von Befehlen verhindern.

Das sind unsere Möglichkeiten:

1:rdtscp ist ein Serialisierungsaufruf. Es verhindert, dass der Aufruf von rdtscp umgeordnet wird.

__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

rdtscp ist jedoch nur auf neueren CPUs verfügbar. In diesem Fall müssen wir also rdtsc verwenden. Aber rdtsc ist nicht serialisierend. Wenn Sie es also alleine verwenden, kann die CPU es nicht neu ordnen.

Wir können also eine dieser beiden Optionen verwenden, um eine Neuordnung zu verhindern:

2: Dies ist ein Aufruf von cpuid und dann rdtsc. cpuid ist ein Serialisierungsaufruf.

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

: Dies ist ein Aufruf von rdtsc mit memory in der Clobber-Liste, der eine Neuordnung verhindert

__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

Mein Verständnis für die 3. Option ist wie folgt:

Anruf tätigen __volatile__ verhindert, dass der Optimierer den ASM entfernt oder über Befehle bewegt, die die Ergebnisse des ASM benötigen (oder die Eingaben ändern). Es könnte sich jedoch immer noch in Bezug auf nicht verwandte Operationen bewegen. So __volatile__ ist nicht genug.

Teilen Sie dem Compiler mit, dass der Speicher voll ist: : "memory"). Das "memory" clobber bedeutet, dass GCC keine Annahmen darüber treffen kann, dass der Speicherinhalt im gesamten Asm gleich bleibt, und daher nicht um ihn herum neu anordnet.

Meine Fragen sind also:

  • 1: Ist mein Verständnis von __volatile__ und "memory" richtig?
  • 2: Tun die zweiten beiden Anrufe dasselbe?
  • 3: Mit "memory" sieht viel einfacher aus als die Verwendung eines anderen Serialisierungsbefehls. Warum sollte jemand die 3. Option anstelle der 2. Option verwenden?
59
Steve Lorimer

Wie in einem Kommentar erwähnt, gibt es einen Unterschied zwischen einem Compiler-Barriere und einem Prozessor-Barriere. volatile und memory in der asm-Anweisung fungieren als Compiler-Barriere, der Prozessor kann jedoch weiterhin Anweisungen neu anordnen.

Prozessorsperre sind spezielle Anweisungen, die ausdrücklich angegeben werden müssen, z. rdtscp, cpuid, Anweisungen für den Speicherbereich (mfence, lfence, ...) usw.

Abgesehen davon kann die Verwendung von cpuid als Barriere, bevor rdtsc üblich ist, auch unter Performance-Gesichtspunkten sehr schlecht sein, da virtuelle Maschinenplattformen häufig die cpuid abfangen und emulieren. ], um einen gemeinsamen Satz von CPU-Funktionen für mehrere Computer in einem Cluster festzulegen (um sicherzustellen, dass die Livemigration funktioniert). Daher ist es besser, eine der Anweisungen für den Speicherbereich zu verwenden.

Der Linux-Kernel verwendet mfence;rdtsc auf AMD-Plattformen und lfence;rdtsc auf Intel. Wenn Sie sich nicht mit der Unterscheidung zwischen diesen beschäftigen möchten, funktioniert mfence;rdtsc auf beiden, obwohl es etwas langsamer ist, da mfence eine stärkere Barriere ist als lfence.

42
janneb

sie können es wie unten gezeigt verwenden:

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");

Im obigen Code implementiert der erste CPUID-Aufruf eine Sperre, um eine nicht ordnungsgemäße Ausführung der Anweisungen über und unter der RDTSC-Anweisung zu vermeiden. Mit dieser Methode vermeiden wir, einen CPUID-Befehl zwischen den Lesevorgängen der Echtzeitregister aufzurufen

Der erste RDTSC liest dann das Zeitstempelregister und der Wert wird im Speicher gespeichert. Dann wird der Code ausgeführt, den wir messen möchten. Der RDTSCP-Befehl liest das Zeitstempelregister zum zweiten Mal und stellt sicher, dass die Ausführung des gesamten Codes, den wir messen wollten, abgeschlossen ist. Die beiden folgenden “mov” -Anweisungen speichern die edx- und eax-Registerwerte im Speicher. Schließlich garantiert ein CPUID-Aufruf, dass eine Sperre erneut implementiert wird, so dass es unmöglich ist, dass ein danach kommender Befehl vor der CPUID selbst ausgeführt wird.

5
Pranjal Verma