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 OS

    Du 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

    zb hier, hier oder hier.

    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.


Anmelden zum Antworten