2012-09-28 5 views
50

Angenommen, wir versuchen, den TSC zur Leistungsüberwachung zu verwenden, und wir wollen verhindern, dass Befehle neu angeordnet werden.Unterschied zwischen Rdtscp, Rdtsc: Speicher und CPU/Rdtsc?

Dies sind unsere Optionen:

1:rdtscp ist eine Serialisierung Anruf. Es verhindert eine Neuanordnung des Aufrufs von 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 

ist jedoch rdtscp nur auf neueren CPUs zur Verfügung. In diesem Fall müssen wir rdtsc verwenden. Aber rdtsc ist nicht serialisierend, so dass es die CPU nicht daran hindert, sie neu zu ordnen.

So können wir eine dieser beiden Optionen verwenden, um Neuanordnung zu verhindern:

2: Dies ist ein Aufruf an 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 

3: Dies ist ein Aufruf an rdtsc mit memory in der clobber Liste, die

Neuordnungs 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 dritte Option ist wie folgt:

die Herstellung Der Aufruf __volatile__ verhindert, dass der Optimierer die ASM entfernt oder sie über irgendwelche Anweisungen bewegt, die die Ergebnisse (oder die Eingaben) der ASM benötigen könnten. Es könnte jedoch immer noch in Bezug auf nicht verwandte Operationen verschoben werden. Also __volatile__ ist nicht genug.

Sagen Sie dem Compilerspeicher wird geplätschert: : "memory"). Der "memory" Clobber bedeutet, dass GCC keine Annahmen über Speicherinhalte treffen kann, die über die asm gleich bleiben und daher nicht um sie herum neu angeordnet werden.

So sind meine Fragen:

  • 1: Ist mein Verständnis von __volatile__ und "memory" richtig?
  • 2: Machen die zweiten beiden Anrufe dasselbe?
  • 3: Die Verwendung von "memory" sieht viel einfacher aus als die Verwendung einer anderen Serialisierungsanweisung. Warum sollte jemand die dritte Option über die zweite Option verwenden?
+9

Sie scheinen die Neuordnung der vom Compiler generierten Anweisungen zu verwirren, was Sie vermeiden können, indem Sie 'volatile' und' memory' verwenden und die Anweisungen des Prozessors neu anordnen (aka _out of order execution_), die Sie mit 'vermeiden CPUuid. – hirschhornsalz

+0

@hirschhornsalz aber nicht "Speicher" in der Clobber-Liste verhindern, dass der Prozessor die Anweisungen neu anordnen? Funktioniert "Gedächtnis" nicht wie ein Erinnerungszaun? –

+0

oder vielleicht wird der "Speicher" in der Clobber-Liste nur an gcc ausgegeben, und der resultierende Maschinencode setzt dies dem Prozessor nicht aus? –

Antwort

35

Wie in einem Kommentar erwähnt, gibt es einen Unterschied zwischen einer Compiler Barriere und einer Prozessor Barriere. volatile und memory in der asm-Anweisung fungieren als eine Compiler-Barriere, aber der Prozessor ist immer noch frei, Anweisungen neu zu ordnen.

Prozessorbarriere sind spezielle Anweisungen, die explizit angegeben werden müssen, z. rdtscp, cpuid, Speicherzaun Anweisungen (mfence, lfence, ...) usw.

Als Nebenwirkung bei der Verwendung von cpuid als Barriere vor rdtsc üblich ist, kann es auch aus Sicht der Leistung sehr schlecht, da virtueller Maschine Plattformen oft Trap und emulieren die cpuid Anweisung, um einen gemeinsamen Satz von CPU zu verhängen Features auf mehreren Computern in einem Cluster (um sicherzustellen, dass Live-Migration funktioniert). Daher ist es besser, eine der Speicherzaun-Anweisungen zu verwenden.

Der Linux-Kernel verwendet mfence;rdtsc auf AMD-Plattformen und lfence;rdtsc auf Intel. Wenn Sie sich nicht damit auseinandersetzen wollen, funktioniert mfence;rdtsc auf beiden, obwohl es etwas langsamer ist, da mfence eine stärkere Barriere als lfence ist.

+5

Die 'cpuid; rdtsc' geht es nicht um Speicherzäune, es geht darum, den Befehlsstrom zu serialisieren. Normalerweise wird es für Benchmarkzwecke verwendet, um sicherzustellen, dass keine "alten" Anweisungen in der Umordnungspuffer-/Reservierungsstation verbleiben. Die Ausführungszeit von 'cpuid' (die ziemlich lang ist, ich erinnere mich an> 200 Zyklen) soll dann subtrahiert werden. Wenn das Ergebnis "genauer" ist, ist dieser Weg für mich nicht ganz klar, experimentierte ich mit und ohne und die Unterschiede scheinen weniger der natürliche Fehler der Messung zu sein, selbst im Einzelbenutzer-Modus, bei dem sonst nichts läuft. – hirschhornsalz

+0

Ich bin mir nicht sicher, aber ich möglicherweise die Zaun-Anweisung auf diese Weise im Kernel verwendet sind überhaupt nicht nützlich ^^ – hirschhornsalz

+4

@hirschhornsalz: Nach den Git-Commit-Logs, AMD und Intel bestätigt, dass die m/lfence derzeit rdtsc serialisieren wird verfügbare CPUs. Ich nehme an, Andi Kleen kann mehr Details darüber liefern, was genau gesagt wurde, wenn Sie interessiert sind und ihn fragen. – janneb

5

Sie können es unten verwenden wie:

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

In dem obigen Code implementiert der erste CPUID Aufruf eine Barriere out-of-order oberhalb und unterhalb der RDTSC Anweisung der Ausführung der Befehle zu vermeiden. Mit dieser Methode vermeiden wir den Aufruf eines CPUID-Befehls zwischen den Lesevorgängen der Echtzeitregister.

Das erste RDTSC liest dann das Zeitstempelregister und der Wert wird im Speicher gespeichert. Dann wird der Code, den wir messen wollen, ausgeführt. Der RDTSCP-Befehl liest das Zeitstempelregister zum zweiten Mal und garantiert, dass die Ausführung des gesamten Codes, den wir messen wollten, abgeschlossen ist. Die zwei "mov" -Anweisungen, die danach kommen, speichern die edx- und eax-Registerwerte im Speicher. Schließlich garantiert ein CPUID-Aufruf, dass eine Barriere erneut implementiert wird, so dass es unmöglich ist, dass ein später kommender Befehl vor der CPUID selbst ausgeführt wird.

+12

Hallo, es scheint, dass Sie diese Antwort von Gabriele Paolinis White Paper "How to Benchmark Code Ausführungszeiten auf Intel® IA-32 und IA-64 Instruction Set Architekturen" kopiert (Sie haben jedoch einen Zeilenumbruch verpasst). Sie verwenden die Arbeit eines anderen, ohne dem Autor etwas zu verraten. Warum nicht eine Zuschreibung hinzufügen? –

+0

Ja, tatsächlich wird es bewältigt. Ich frage mich auch, ob die beiden movs beim Lesen der Startzeit notwendig sind: http://stackoverflow.com/questions/38994549/is-intels-timestamp-reading-asm-code-example-mit-zwei weiteren Registern -than-sind –

+0

Gibt es einen bestimmten Grund, zwei Variablen hoch und niedrig zu haben? – ExOfDe