Takte messen für mov und xor
-
Hallo,
es gibt mehrere Möglichkeiten, auf x86 CPUs ein Register auf Null zu setzen. Die einfachste davon ist:
movl $0, %eax
Und die angeblich perfomantere Möglichkeit:
xorl %eax, %eax
Nun möchte ich den Unterschied messen. Habe mir zwei Funktionen in Assembler zusammengefrickelt (Datei rdtsc.s):
.global _getTimeStampCounterA .global _getTimeStampCounterB .section .text _getTimeStampCounterA: pushl %ebx movl $0, %eax movl $0, %eax movl $0, %eax movl $0, %eax movl $0, %eax movl $0, %eax movl $0, %eax movl $0, %eax movl $0, %eax movl $0, %eax movl $0, %eax movl $0, %eax movl $0, %eax movl $0, %eax movl $0, %eax movl $0, %eax movl $0, %eax movl $0, %eax movl $0, %eax movl $0, %eax movl $0, %eax movl $0, %eax movl $0, %eax movl $0, %eax movl $0, %eax movl $0, %eax movl $0, %eax movl $0, %eax movl $0, %eax movl $0, %eax movl $0, %eax movl $0, %eax cpuid # serialize rdtsc popl %ebx ret _getTimeStampCounterB: pushl %ebx xorl %eax, %eax xorl %eax, %eax xorl %eax, %eax xorl %eax, %eax xorl %eax, %eax xorl %eax, %eax xorl %eax, %eax xorl %eax, %eax xorl %eax, %eax xorl %eax, %eax xorl %eax, %eax xorl %eax, %eax xorl %eax, %eax xorl %eax, %eax xorl %eax, %eax xorl %eax, %eax xorl %eax, %eax xorl %eax, %eax xorl %eax, %eax xorl %eax, %eax xorl %eax, %eax xorl %eax, %eax xorl %eax, %eax xorl %eax, %eax xorl %eax, %eax xorl %eax, %eax xorl %eax, %eax xorl %eax, %eax xorl %eax, %eax xorl %eax, %eax xorl %eax, %eax xorl %eax, %eax cpuid # serialize rdtsc popl %ebx ret
Und den Testcode dazu (Datei main.c):
#include <stdio.h> typedef unsigned long long uint64; extern uint64 getTimeStampCounterA(); extern uint64 getTimeStampCounterB(); int main(int argc, char *argv[]) { unsigned int i = 0u; uint64 a = 0u; uint64 b = 0u; argc = argc; argv = argv; printf("\nMethod B:\n"); for (i = 0u; i < 20u; ++i) { a = getTimeStampCounterB(); b = getTimeStampCounterB(); printf("b - a = %u\n", (unsigned int)(b - a)); } printf("\nMethod A:\n"); for (i = 0u; i < 20u; ++i) { a = getTimeStampCounterA(); b = getTimeStampCounterA(); printf("b - a = %u\n", (unsigned int)(b - a)); } return 0; }
Assemblieren und kompilieren tue ich mit mingw gcc und binutils, mein Makefile:
all: as rdtsc.s -o rdtsc.o gcc -c main.c -o main.o -O3 -pedantic -Wall -Wextra gcc rdtsc.o main.o -o main.exe
Nun macht mein Testcode folgende Ausgabe:
b - a = 286 b - a = 286 b - a = 275 b - a = 264 b - a = 275 b - a = 275 b - a = 286 b - a = 264 b - a = 275 b - a = 275 b - a = 5104 b - a = 275 b - a = 275 b - a = 275 b - a = 275 b - a = 275 b - a = 275 b - a = 275 b - a = 275 b - a = 275 b - a = 286 b - a = 286 b - a = 286 b - a = 297 b - a = 275 b - a = 275 b - a = 275 b - a = 275 b - a = 517 b - a = 495 b - a = 275 b - a = 275 b - a = 275 b - a = 275 b - a = 275 b - a = 275 b - a = 275 b - a = 275 b - a = 286 b - a = 286
Ich habe absichtlich die Zeilen "Method X" in der Ausgabe gelöscht, damit man nicht sieht, zu welchen Funktionen die Ausgabe gehört.
Irgendwie erkenne ich nicht den Unterschied Messe ich irgendwas falsch? Habe ich irgendwo einen Fehler gemacht? Kann man den Unterschied zwischen mov und xor irgendwie anders messsen?
-
Beide Instruktionen werden theoretisch in einem Taktzyklus ausgefuehrt.
"movl $0, %eax" beinhaltet dabei jedoch zusaetzlich den Wert "0" (32bit) und ist somit groesser.
"rdtsc" liesst den Zykluszaehler, dieser beinhaltet bei Deiner Messung nicht nur die Zyklen Deienr 32 Instruktionen sondern auch
- die benoetigt Zyklen um Deine Funktion anzuspringen
- den Overhead der Hochsprache (Register sichern usw)
- Unterbrechungen vom OSDu solltest also deutlich groessere Codestuecke messen damit die Verfaelschungen im Verhaeltnis dazu klein werden.
Generell solltest Du bedenken, dass die x86 Instruktionen von der CPU nicht mehr 1:1 sequentiell ausgefuehrt werden sondern vom Frontend an parallele Recheneinheiten (die teilweise mehrmals vorhanden sind) verteilt werden.
Es kommt also viel mehr auf eine ideale Gesamtauslastung des Rechenwerks an als darauf, jeweils den "schnellsten" Opcode zu waehlen.
-
abc.w:
Du machst dir da aber echt Gedanken drueber, oder?
Hast du die ganzen Antworten dazu im OS-Thread gelesen?
-
Mir ist schon klar, dass man es nicht taktgenau messen kann.
Habe noch ein wenig gemessen, hin und her, mal dies, mal jenes ausprobiert. Mein Fazit: Der Unterschied ist irgendwie verschwindend klein. Tendenz hinsichtlich weniger Takte geht in Richtung des Befehls xor, aber nicht so, dass ich sagen würde, wow, ist das schnell. Wundert mich jetzt ein wenig, denn, theoretisch abgeschätzt wäre xor höchstens doppelt schneller als mov, also 50%. Praktisch hätte ich erwartet 10%-15%. Aber dass man nur 2%-3% Prozent misst, hm, komisch. Vielleicht teste ich es falsch....global _getTimeStampCounterA .global _getTimeStampCounterB .equ MAX_LOOP_CNT, 0xFFFFFFFF .section .text _getTimeStampCounterA: pushl %ebx movl $MAX_LOOP_CNT, %ecx 0: movl $0, %eax dec %ecx jnz 0b cpuid # serialize rdtsc popl %ebx ret _getTimeStampCounterB: pushl %ebx movl $MAX_LOOP_CNT, %ecx 0: xorl %eax, %eax dec %ecx jnz 0b cpuid # serialize rdtsc popl %ebx ret
-
abc.w schrieb:
Hallo,
es gibt mehrere Möglichkeiten, auf x86 CPUs ein Register auf Null zu setzen. Die einfachste davon ist:movl $0, %eax
Und die angeblich perfomantere Möglichkeit:
xorl %eax, %eax
Nun möchte ich den Unterschied messen.
da brauchst du nix zu messen. wie viele takte ein befehl hat, ist irgendwo dokumentiert und wenn du deine taktfrequenz weisst, rechneste für die zeit pro befehl einfach: 1/cpu_takt * anzahl_takte_pro_befehl.
-
wieso lässt du die jeweiligen operationen nicht einfach paar milliarden male durchlaufen und misst dann die jeweilige zeit mit der QueryPerformanceCounter Function:
http://msdn.microsoft.com/en-us/library/ms644904(VS.85).aspx
-
+fricky schrieb:
da brauchst du nix zu messen. wie viele takte ein befehl hat, ist irgendwo dokumentiert und wenn du deine taktfrequenz weisst, rechneste für die zeit pro befehl einfach: 1/cpu_takt * anzahl_takte_pro_befehl.
Ich stelle mich eben dumm und verstehe die Doku nicht. Wo findet man übrigens diesbezüglich ein Dokument, habe in meinem Laptop eine Centrino Duo 1.8GHz CPU
Eins muss man aber sagen, es gibt tatsächlich einen Fall, wo xor schneller ist als mov, bei mir nämlich bei diesem Code:.global _getCurrentTimeStampCounter .global _getTimeStampCounterA .global _getTimeStampCounterB .equ MAX_LOOP_CNT, 1000000 .section .text _getCurrentTimeStampCounter: rdtsc ret _getTimeStampCounterA: pushl %ebx movl $MAX_LOOP_CNT, %ecx 0: movl $0, %eax movl $0, %ebx movl $0, %eax dec %ecx jnz 0b cpuid # serialize rdtsc popl %ebx ret _getTimeStampCounterB: pushl %ebx movl $MAX_LOOP_CNT, %ecx 0: xorl %eax, %eax xorl %ebx, %ebx xorl %eax, %eax dec %ecx jnz 0b cpuid # serialize rdtsc popl %ebx ret
Testcode sieht nun so aus:
#include <stdio.h> typedef unsigned long long uint64; extern uint64 getCurrentTimeStampCounter(void); extern uint64 getTimeStampCounterA(void); extern uint64 getTimeStampCounterB(void); int main(int argc, char *argv[]) { unsigned int i = 0u; uint64 a = 0u; uint64 b = 0u; uint64 c = 0u; argc = argc; argv = argv; printf("\nMethod B:\n"); for (i = 0u; i < 20u; ++i) { a = getCurrentTimeStampCounter(); b = getTimeStampCounterB(); c = (b - a); printf("%llu\n", c); } printf("\nMethod A:\n"); for (i = 0u; i < 20u; ++i) { a = getCurrentTimeStampCounter(); b = getTimeStampCounterA(); c = (b - a); printf("%llu\n", c); } return 0; }
Wenn man eine Centrino Duo CPU hat, dann lohnt es sich, drei mal hintereinander irgendwelche Register mit xor auf Null zurücksetzen. Ansonsten bleibt xor für mich eine "Optimize for size" Möglichkeit...
-
abc.w schrieb:
Ansonsten bleibt xor für mich eine "Optimize for size" Möglichkeit...
Und da Size auch Performance ist, wie wir Assembler hacker alle wissen: q.e.d.
-
abc.hirn schrieb:
Und da Size auch Performance ist, wie wir Assembler hacker alle wissen: q.e.d.
sub eax,eax
-
nice
-
abc.hirn schrieb:
Und da Size auch Performance ist, wie wir Assembler hacker alle wissen: q.e.d.
Na ja, geh heim und programmier ruhig weiter Deine Z80 oder 8085 oder 4004...
asm.hack schrieb:
sub eax,eax
Du meinst wohl das hier ;):
subl %eax, %eax
Habs ausprobiert. Das Verhalten ist genauso wie bei xor, bei drei mal sub plötzlich schneller, sonst genauso schnell... Sprich, Optimize for size
-
abc.w schrieb:
abc.hirn schrieb:
Und da Size auch Performance ist, wie wir Assembler hacker alle wissen: q.e.d.
Na ja, geh heim und programmier ruhig weiter Deine Z80 oder 8085 oder 4004...
Das hat heute mehr Sinn als damals.
-
wie viele takte ein befehl hat, ist irgendwo dokumentiert
Wo findet man übrigens diesbezüglich ein Dokument
Das Verhalten ist genauso wie bei xor, bei drei mal sub plötzlich schneller, sonst genauso schnell...
Wie ich bereits anfangs schrieb kommt es auf den Code-Mix an - sind gerade alle Addierwerke belegt ist xor eben sinnvoller.
-
hellihjb schrieb:
...Wie ich bereits anfangs schrieb kommt es auf den Code-Mix an - sind gerade alle Addierwerke belegt ist xor eben sinnvoller.
Ja, das kann sein. Ist aber ein interessantes Verhalten, ich habe die Werte ein wenig grafisch zusammengestellt (Anzahl der Befehle und benötigte Takte grosszügig gerundet dazu), alles schön linear, nur dieser eine Knick bei 3 Befehlen hintereinander...
-
...wie sieht denn so eine 3er-Kombination von sub oder xor mit umgebenden Mess-Code bei dir dann aus?
-
Das ist die Datei rdtsc.s mit den vier Funktionen:
.global _getCurrentTimeStampCounter .global _getTimeStampCounterA .global _getTimeStampCounterB .global _getTimeStampCounterC .equ MAX_LOOP_CNT, 1000000 .section .text _getCurrentTimeStampCounter: rdtsc ret _getTimeStampCounterA: pushl %ebx movl $MAX_LOOP_CNT, %ecx 0: movl $0, %eax movl $0, %eax movl $0, %eax dec %ecx jnz 0b cpuid # serialize rdtsc popl %ebx ret _getTimeStampCounterB: pushl %ebx movl $MAX_LOOP_CNT, %ecx 0: xorl %eax, %eax xorl %eax, %eax xorl %eax, %eax dec %ecx jnz 0b cpuid # serialize rdtsc popl %ebx ret _getTimeStampCounterC: pushl %ebx movl $MAX_LOOP_CNT, %ecx 0: subl %eax, %eax subl %eax, %eax subl %eax, %eax dec %ecx jnz 0b cpuid # serialize rdtsc popl %ebx ret
Das ist meine main.c:
#include <stdio.h> #define MAX_TEST_LOOP 10u typedef unsigned long long uint64; extern uint64 getCurrentTimeStampCounter(void); extern uint64 getTimeStampCounterA(void); extern uint64 getTimeStampCounterB(void); extern uint64 getTimeStampCounterC(void); int main(int argc, char *argv[]) { unsigned int i = 0u; uint64 a = 0u; uint64 b = 0u; uint64 c = 0u; argc = argc; argv = argv; printf("\nMethod C:\n"); for (i = 0u; i < MAX_TEST_LOOP; ++i) { a = getCurrentTimeStampCounter(); b = getTimeStampCounterC(); c = (b - a); printf("%llu\n", c); } printf("\nMethod B:\n"); for (i = 0u; i < MAX_TEST_LOOP; ++i) { a = getCurrentTimeStampCounter(); b = getTimeStampCounterB(); c = (b - a); printf("%llu\n", c); } printf("\nMethod A:\n"); for (i = 0u; i < MAX_TEST_LOOP; ++i) { a = getCurrentTimeStampCounter(); b = getTimeStampCounterA(); c = (b - a); printf("%llu\n", c); } return 0; }
Und das ist mein Makefile:
all: as rdtsc.s -o rdtsc.o gcc -c main.c -o main.o -O3 -pedantic -Wall -Wextra gcc rdtsc.o main.o -o main.exe
Und das ist die Ausgabe:
Method C: 2739286 2585924 2585957 2599179 2668897 2599190 2599179 2599179 2599179 2599179 Method B: 2585924 2599256 2593459 2599256 2588817 2622829 2585924 2588817 2588817 2588817 Method A: 3000327 3000327 3000327 3000316 3000327 3025671 3000338 3000283 3000283 3000283
Die Funktionen getTimeStampCounterB() (mit 3 mal xor) und getTimeStampCounterC() (mit 3 mal sub) sind eindeutig um etwa halbe Million Takte schneller als die Funktion getTimeStampCounterA() (mit 3 mal mov)...
-
...sind eindeutig um etwa halbe Million Takte schneller
Und zum Vergleich mit einem Core2:
Method C: 2000130 2000130 2000097 2051247 2000262 2000086 2000086 2000086 2000097 2000097 Method B: 2000141 2000130 2000097 2000097 2000097 2000097 2000086 2000097 2000097 2000097 Method A: 2001417 2000130 2000086 2000086 2000097 2000086 2000097 2000097 2000086 2000097
-
@hellihjb:
Das ist im Prinzip das, was ich auch beobachte. Alles gleich. Nur in diesem einen Sonderfall mit 3 gleichen Befehlen hintereinander gibt es diesen einen Unterschied. Meine CPU ist Centrino Duo.
Wie ich bereits sagte, habe in den Schleifen jeweils ein mov, xor, sub Befehl, zwei mov, xor, sub Befehle, drei mov, xor, sub Befehle usw. ausprobiert und die Werte mal grafisch zusammengestellt. Alles gleich und alles ist linear, nur ausgerechnet bei Schleifen mit drei xor und drei sub gibt es diesen einen Ausreisser...
-
nur ausgerechnet bei Schleifen mit drei xor und drei sub gibt es diesen einen Ausreisser...
Dass die Cpu bei aufeinanderfolgenden Instruktionen mit gleichem Zielregister Schwierigkeiten mit deren Parallelisierung haben kann ist nicht verwunderlich.
Wenn Du Dir bei Intel mal ein paar Dokumente zum Cpu-Frontend, Instruction-Prefetching und der Zerlegung von x86-Instruktionen in Micro-Ops anschaust, wirst Du sicherlich Hinweise darauf finden wieso es mit Deiner Cpu bei "mov" nicht klappt.