Schnelligkeit von C-Code



  • Achtung, an alle Foren-Neulinge: Dies ist der inoffizielle Sandkasten-Spielplatz-Thread!
    Nach dem Motto: Viel Rauch um Nichts, noch mehr Rauch um Überhauptnichts!

    MfG
    Alter C-Fuchs

    🙂



  • register hat den Nachteil, dass es sich hier um eine Kann-Optimierung des Compilers handelt (was naturgemäß wenig erstrebenswert ist wegen unnötiger externer Abhängigkeiten und Unportabilität) und weil die Adressierung der Variablen unmöglich gemacht wird:

    http://ideone.com/kXPJqG

    #include <stdio.h>
    void run(int *i)
    {
    	printf("%d",*i);
    }
    
    int main(void) {
    	register int i = 04711;
    	run(&i);
    	return 0;
    }
    
    prog.c:9:2: error: address of register variable 'i' requested
      run(&i);
      ^
    


  • register-variablen haben ja auch gar keine adresse?!
    wenn von mehreren stellen darauf zugegriffen werden soll, wäre es evtl eine lösung, die variable global anzulegen.

    ich finds btw. sehr lehrreich. 👍


  • Mod

    HansKlaus schrieb:

    wenn von mehreren stellen darauf zugegriffen werden soll, wäre es evtl eine lösung, die variable global anzulegen.

    Global kann sie garantiert nicht mehr in einem Register gehalten werden. Totale Anti-Optimierung.



  • SeppJ schrieb:

    Global kann sie garantiert nicht mehr in einem Register gehalten werden. Totale Anti-Optimierung.

    Das hängt vom Compiler ab - der GCC kann sowas:

    #include <stdio.h>
    
    register int i asm("r15");
    
    int main(void)
    {
        i = 0;
        printf("%d\n",i);
        return 0;
    }
    

    Man sollte dabei aber auch ein Register wählen, welches keinen ABI-Regeln unterliegt. Sonst meckert der Compiler:

    GCC schrieb:

    für Ruf vorgesehenes Register wurde für globale Registervariable verwendet

    Wie sinnvoll es ist, eine einzige Variable für jeden Thread in einem Register zu halten, lassen wir mal dahingestellt.



  • @HansKlaus

    kannst du meine Watcom Ergebnisse irgendwie erklären (bei Nutzung der Optimierflags) - hab ihr damals den resultierenden Maschinencode der C- und Asm-Lösung verglichen oder einfach nur fest gestellt das es schneller ist? kann es sein das ihr ohne Optimierung gebaut habt?


  • Mod

    dachschaden schrieb:

    SeppJ schrieb:

    Global kann sie garantiert nicht mehr in einem Register gehalten werden. Totale Anti-Optimierung.

    Das hängt vom Compiler ab - der GCC kann sowas:

    Ja, wenn er das ganze Programm vorliegen hat. Aber probier es mal aus, wenn du mehr als eine Datei hast.


  • Mod

    Gast3 schrieb:

    kannst du meine Watcom Ergebnisse irgendwie erklären (bei Nutzung der Optimierflags) - hab ihr damals den resultierenden Maschinencode der C- und Asm-Lösung verglichen oder einfach nur fest gestellt das es schneller ist? kann es sein das ihr ohne Optimierung gebaut habt?

    Sie haben halt einmal von Hand geinlined und beim anderen Mal eine für den Compiler opaque Funktion aufgerufen. Sehr überraschendes Ergebnis 🙄



  • SeppJ schrieb:

    Ja, wenn er das ganze Programm vorliegen hat. Aber probier es mal aus, wenn du mehr als eine Datei hast.

    Habe ich gerade - zwei unterschiedliche Variablen auf das gleiche Register, die in unterschiedlichen TUs vorliegen. Kompiliert und linkt ohne Probleme, die Variable verhält sich dann wie mit externem Linkage - wenn ich in eine Variable schreibe, ist das auch in der anderen sichtbar.

    EDIT: Ich sollte dazu sagen, dass mich das nicht überrascht. Warum auch? Das Register gibt es ja nur einmal.


  • Mod

    dachschaden schrieb:

    SeppJ schrieb:

    Ja, wenn er das ganze Programm vorliegen hat. Aber probier es mal aus, wenn du mehr als eine Datei hast.

    Habe ich gerade - zwei unterschiedliche Variablen auf das gleiche Register, die in unterschiedlichen TUs vorliegen. Kompiliert und linkt ohne Probleme, die Variable verhält sich dann wie mit externem Linkage - wenn ich in eine Variable schreibe, ist das auch in der anderen sichtbar.

    Wow, Respekt vor dieser Fertigkeit des GCC.

    EDIT: Ich sollte dazu sagen, dass mich das nicht überrascht. Warum auch? Das Register gibt es ja nur einmal.

    Weil das heißt, dass er mal eben ein Register permanent reserviert, für den gesamten Programmablauf? Er kann ja schließlich nie was anderes da rein packen, da er nie sicher sein kann, ob die Variable nicht anderswo benutzt wird. Und er muss dafür sorgen, dass keine andere Codeeinheit jemals dieses Register belegt, selbst wenn sie von dieser Variable gar nichts weiß. Und er muss sich über mehrere Codeeinheiten absprechen, welches Symbol welches Register bezeichnet, während ansonsten später der Linker Beziehungen dieser Art herstellt.

    Irgendwie kann ich immer noch nicht glauben, dass das funktionieren soll. Hast du irgendeine Form von programmübergreifender Optimierung gemacht?



  • SeppJ schrieb:

    Er kann ja schließlich nie was anderes da rein packen, da er nie sicher sein kann, ob die Variable nicht anderswo benutzt wird.

    Habe mir gerade den Binärcode dieses kleinen Programms angeschaut:

    main.c:

    #include <stdio.h>
    
    /*Freie Register "blockieren."*/
    register size_t i_12 asm("r12");
    register size_t i_13 asm("r13");
    register size_t i_14 asm("r14");
    register size_t i_15 asm("r15");
    
    /*Funktion aus dem anderen Modul.*/
    void func(void);
    
    int main(void)
    {
        /*Und jetzt mal ein paar Register-Variablen fordern.*/
        register size_t s_0 = 0;
        register size_t s_1 = 1;
        register size_t s_2 = 2;
        register size_t s_3 = 3;
        register size_t s_4 = 4;
    
        /*Canaries ablegen*/
        i_12 = 0;
        i_13 = 1;
        i_14 = 2;
        i_15 = 3;
    
        /*Canaries anzeigen*/
        printf("%lu|%lu|%lu|%lu\n",i_12,i_13,i_14,i_15);
    
        /*Funktionsaufruf.*/
        func();
    
        /*Wurden die Canaries geändert?*/
        printf("%lu|%lu|%lu|%lu\n",i_12,i_13,i_14,i_15);
    
        return 0;
    }
    

    func.c:

    #include <stdio.h>
    
    void func(void)
    {
        /*Lose und wild Register reservieren.*/
        register size_t s_0 = 4;
        register size_t s_1 = 3;
        register size_t s_2 = 2;
        register size_t s_3 = 1;
        register size_t s_4 = 0;
    
        /*Sicherstellen, dass wir jetzt andere Werte haben.*/
        printf("%lu|%lu|%lu|%lu|%lu\n",s_0,s_1,s_2,s_3,s_4);
    }
    

    Ausgabe:

    0|1|2|3
    4|3|2|1|0
    0|1|2|3
    

    Variablenerstellung in Main:

    mov    r12d,0x0
    mov    r13d,0x1
    mov    r14d,0x2
    mov    r15d,0x3
    mov    rsi,r15
    mov    rcx,r14
    mov    rdx,r13
    mov    rax,r12
    mov    r8,rsi
    mov    rsi,rax
    

    Danach kommt der erste printf -Call. Wir haben also sichergestellt, dass unsere Registervariablen auch wirklich in den Registern liegen.

    Nach printf kommt der Aufruf von func :

    push   r15
    push   r14
    push   r13
    push   r12
    push   rbx
    

    Die Register, die die Funktion gerne reservieren möchte, werden also auf dem Stack zwischengespeichert. Und das ist auch der Fall, wenn ich func.c separat kompilieren lasse in func.obj.

    Sprich, der Compiler geht keine Risiken ein und sichert sich immer den Wert der reservierten Register auf dem Stack.

    SeppJ schrieb:

    Irgendwie kann ich immer noch nicht glauben, dass das funktionieren soll. Hast du irgendeine Form von programmübergreifender Optimierung gemacht?

    Nein, auch keine -O-Switches aktiviert. LTO vertraue ich ohnehin nicht.

    Aber aktivieren wir mal -O3 für das separate Kompilieren von func.c:

    push   0x0
    mov    ecx,0x3
    mov    edx,0x4
    mov    esi,0x0
    mov    edi,0x1
    

    , dann arbeitet der Compiler lieber in den durch das ABI angebotenen freien Registern, und zwar direkt für die Parameterübergabe. Und wie im Vorpost geschrieben, gibt der GCC eine Warnung aus, wenn man versucht, globale Registervariablen mit ABI-reservierten Registern zu assoziieren.

    EDIT: Ist ja auch logisch. Das ABI spezifiziert beim Funktionseintritt, welche Register zur Verfügung stehen und welche gesichert werden müssen. Das ABI kennt der Compiler genau; er muss ABI-konformen Code abliefern, sonst könnten die Interfaces dort draußen ja jederzeit brechen.

    EDIT 2:

    Wikipedia schrieb:

    If the callee wishes to use registers RBP, RBX, and R12–R15, it must restore their original values before returning control to the caller. All others must be saved by the caller if it wishes to preserve their values.


  • Mod

    Aber dann ist es doch keine Registervariable mehr, wenn sie im Hauptspeicher lebt und bei Bedarf in Register geladen wird! Es ist eine ganz normale Variable, für die man ein bevorzugtes Register angegeben hat.



  • SeppJ schrieb:

    Gast3 schrieb:

    kannst du meine Watcom Ergebnisse irgendwie erklären (bei Nutzung der Optimierflags) - hab ihr damals den resultierenden Maschinencode der C- und Asm-Lösung verglichen oder einfach nur fest gestellt das es schneller ist? kann es sein das ihr ohne Optimierung gebaut habt?

    Sie haben halt einmal von Hand geinlined und beim anderen Mal eine für den Compiler opaque Funktion aufgerufen. Sehr überraschendes Ergebnis 🙄

    ich glaube du hast mein Ergebnis falsch interpretiert:

    wenn man HansKlaus Beispiel mit Open Watcom+Optimierung kompiliert ist die inline-Asm-Version langsamer und die C-Version ist kürzer/schneller (inlined + weniger Opcodes)

    ich würde gerne Wissen was damals schief gelaufen ist (z.B. Debug-Build gebenchmarkt) oder ob der Open Watcom viel älter/schlechter war (obwohl an der Optimierung seit Veröffentlichung kaum gearbeitet wurde)
    Ich stolpere häufiger über solche Aussagen bei Kollegen/Azubis und gehen denen dann gerne vollständig auf den Grund um Mythenbildung im Keim zu ersticken oder um wirklich erklären zu können warum es schneller ist

    btw: ist die Nutzung von register ab C++17 verboten



  • SeppJ schrieb:

    Aber dann ist es doch keine Registervariable mehr, wenn sie im Hauptspeicher lebt und bei Bedarf in Register geladen wird! Es ist eine ganz normale Variable, für die man ein bevorzugtes Register angegeben hat.

    Bei modulübergreifenden Programme - ja.
    Aber bei Nutzung innerhalb eines Moduls - insbesondere innerhalb des Moduls, welches main beinhaltet - kann der Compiler Optimierungen durchführen, um r12-r15 nicht zwischenspeichern zu müssen. Wie bei statischen globalen Variablen, die nach Außen hin nicht sichtbar sind - wenn man func in main.c lässt, kann man sehen, dass func die Variablen lieber auf dem Stack lässt, als r12-r15 zwischenzuspeichern.

    Und ich habe mir nochmal die Mühe gemacht, mit LTO zu kompilieren - und damit lässt func dieses Register auch in Ruhe.


  • Mod

    dachschaden schrieb:

    Und ich habe mir nochmal die Mühe gemacht, mit LTO zu kompilieren - und damit lässt func dieses Register auch in Ruhe.

    Schick.

    Jetzt müssen wir nur noch messen, wie hoch der Performanceeinbruch ist, wenn man den Compiler auf diese Weise zwingt, permanent ein Register weniger für alles zur Verfügung zu haben 🙂



  • dachschaden schrieb:

    Wie sinnvoll es ist, eine einzige Variable für jeden Thread in einem Register zu halten, lassen wir mal dahingestellt.

    Ich habe nicht vor, den Sinn und Zweck, oder Performancegewinne oder -Verluste nachzuweisen.
    Mir ging es nur darum, aufzuzeigen, dass der GCC sowas kann.

    Und ich würde mir verdammt gut überlegen, ein Register auf diese Art und Weise zu belegen. Normalerweise weiß der Compiler nämlich, was er tut.



  • @Sepp, @Hansklaus

    Könnt ihr beide bitte ordentlich auf meine noch offene Frage antworten - erst kommt HansKlaus mit dieser x mal schneller mit Watcom und inline-Assembler Sache, gibt mehr Details und beim Nachstellen zeigt sich ein völlig anderes Bild - leicht unbefriedigend wenn dann gar kein Feedback mehr kommt

    @Sepp

    Sie haben halt einmal von Hand geinlined und beim anderen Mal eine für den Compiler opaque Funktion aufgerufen. Sehr überraschendes Ergebnis 🙄

    ich glaube du hast mein Ergebnis falsch interpretiert:

    wenn man HansKlaus Beispiel mit Open Watcom+Optimierung kompiliert ist die inline-Asm-Version langsamer und die C-Version ist kürzer/schneller (inlined + weniger Opcodes)

    Es ist ein wenig überraschend das HansKlaus Aussage nach meinen Tests einfach nicht stimmt

    @Hansklaus

    ich würde gerne Wissen was damals (möglicherweise) schief gelaufen ist (z.B. Debug-Build gebenchmarkt?) oder ob der Open Watcom viel älter/schlechter war (obwohl an der Optimierung seit Veröffentlichung kaum gearbeitet wurde)
    Ich stolpere häufiger über solche Aussagen bei Kollegen/Azubis und gehen denen dann gerne vollständig auf den Grund um Mythenbildung im Keim zu ersticken oder um wirklich erklären zu können warum es schneller ist

    Warum er Unterschied zu deiner Aussage?



  • die programme wurden über com-schnittstelle auf eine von der hochschule selbst gebaute kiste mit einem 386ex embedded übertragen und dort dann ausgeführt.
    das signal wurde gewissermaßen direkt von der cpu abgenommen und mit einem oszilloskop ausgewertet.

    optimierungsflags wurden keine verwendet und der compiler war evtl. schon etwas älter - eben der, der sich da im labor auf dem rechner befand.

    aber wenn ich mir die assemblercodes so ansehe, ist der inline-assembler nur deshalb langsamer, weil der programmierer offenbar der meinung war, dass das register mit der portadresse jedes mal neu beschrieben werden muss. 🙄

    edit: habe es geschafft, mir den openwatcom compiler raufzuziehen und die programme alle noch einmal durch den compiler zu schieben.

    original programmcodes:

    #include <conio.h>
    
    //Aufgabe21.c
    
    #define EVER ;;
    #define LPT 0x338
    
    int main()
    {
    	for(EVER)
    	{
    		outp(LPT,0x01);
    		outp(LPT,0x00);
    	}
    }
    
    #include <conio.h>
    
    //Aufgabe22.c
    
    #define EVER ;;
    #define LPT 0x338
    
    int main()
    {
    	for(EVER)
    	{
    		_asm
    		{
    			mov al,0x01
    			mov dx,LPT
    			out dx,al
    			mov al,0x00
    			mov dx,LPT
    			out dx,al
    		}
    	}
    
    }
    

    D:\Studium\MCT\Labor\Übung 1>wcl -q -ot aufgabe21
    Aufgabe21.c(13): Warning! W138: No newline at end of file

    D:\Studium\MCT\Labor\Übung 1>wcl -q -ot aufgabe22

    D:\Studium\MCT\Labor\Übung 1>wdis aufgabe21
    Module: D:\Studium\MCT\Labor\▄bung 1\Aufgabe21.c
    GROUP: 'DGROUP' CONST,CONST2,_DATA

    Segment: TEXT PARA USE16 0000001A bytes
    0000 main
    :
    0000 B8 04 00 mov ax,0x0004
    0003 E8 00 00 call STK
    0006 52 push dx
    0007 L$1:
    0007 BA 01 00 mov dx,0x0001
    000A B8 38 03 mov ax,0x0338
    000D E8 00 00 call outp

    0010 31 D2 xor dx,dx
    0012 B8 38 03 mov ax,0x0338
    0015 E8 00 00 call outp

    0018 EB ED jmp L$1

    Routine Size: 26 bytes, Routine Base: _TEXT + 0000

    No disassembly errors

    Segment: CONST WORD USE16 00000000 bytes

    Segment: CONST2 WORD USE16 00000000 bytes

    Segment: _DATA WORD USE16 00000000 bytes

    D:\Studium\MCT\Labor\Übung 1>wdis aufgabe22
    Module: D:\Studium\MCT\Labor\▄bung 1\Aufgabe22.c
    GROUP: 'DGROUP' CONST,CONST2,_DATA

    Segment: TEXT PARA USE16 00000019 bytes
    0000 main
    :
    0000 B8 0C 00 mov ax,0x000c
    0003 E8 00 00 call __STK
    0006 53 push bx
    0007 51 push cx
    0008 52 push dx
    0009 56 push si
    000A 57 push di
    000B L$1:
    000B B0 01 mov al,0x01
    000D BA 38 03 mov dx,0x0338
    0010 EE out dx,al
    0011 B0 00 mov al,0x00
    0013 BA 38 03 mov dx,0x0338
    0016 EE out dx,al
    0017 EB F2 jmp L$1

    Routine Size: 25 bytes, Routine Base: _TEXT + 0000

    No disassembly errors

    Segment: CONST WORD USE16 00000000 bytes

    Segment: CONST2 WORD USE16 00000000 bytes

    Segment: _DATA WORD USE16 00000000 bytes

    D:\Studium\MCT\Labor\Übung 1>

    gut das unterprogramm heißt nicht chk sondern outp, aber wenn der assemblercode nicht so schlecht wäre, wäre das programm mit dem inline-assembler noch schneller und kürzer



  • gut das unterprogramm heißt nicht chk sondern outp, aber wenn der assemblercode nicht so schlecht wäre, wäre das programm mit dem inline-assembler noch schneller und kürzer

    wenn unter realistischen Bedingungen verglichen würde ich dir recht geben - aber das habt ihr nicht und ich verstehe nicht was diese Aufgabe aufzeigen sollte

    nur als Info weil du scheinbar keinerlei Problem mit dem Testszenario und den Erkenntnissen daraus zu haben scheinst

    Es gibt um mal mit VStudio Begriffen um sich zu werfen den sog. Debug- und den Release-Mode (oder z.B. sowas wie -O0 und -O2 bei gcc und Konsorten)

    Debug-Mode bedeutet das der Kompiler angewiesen wird möglich wenig Optimierungen durchzuführen damit der Debugger in Normalfall alle Symbole die im Quelltext vorkommen auch finden und zuordnen kann - als Nebeneffekt wird der Code dadurch auch schneller kompiliert ist aber sehr sehr langsam, der generierte Code sieht fast so aus als hätte man jede einzelne Zeile für sich in Assembler übersetzt - sehr stupider langer Code

    Release-Mode bedeutet das der Kompiler angewiesen wird möglichst schnellen Code zu erzeugen d.h. inlining von kurzen Funktionen, Konstantfolding und,und,und...(die Liste der Optimerungen ist lang und wird ständig länger) - Nachteil sind das der Kompiliervorgang um ein vielfaches länger dauern kann und das Kompilat ist schwerer zu debuggen weil einfach viele Symbole durch die Optimierung wegoptimiert(z.B. mit anderen vereint) werden und der Debugger dann "kann nicht aufgelöst werde..." Anzeigt - dafür ist der Code aber sehr sehr schnell - die Geschwindigkeitssteigerung gegenüber dem Debug-Mode sind normalerweise sehr drastisch

    Es gibt viele Anfänger (hab aber auch schon gestandene Entwickler erlebt) die das nicht wissen und denke der Kompiler wird wohl immer das beste Ergebnis erzeugen - was bei keinem C/C++ Kompiler so zutrifft



  • 2. Teil - zu früh auf Absenden geklickt

    ihr habt also den Kompiler gezwungen (aus Geschwindigkeitssicht) möglichst schlechten Code zu erzeugen (weil keine Optmierung gefordert wrude) und habt das dann mit einem handwerklich gutem inline-Assembler-Coder verglichen - das alter des Watcom Kompiler ist dafür unrelevant weil die Nicht-Optimierung sowieso immer maximal einfachen/langsamen Code erzeugt damit der Debugger besser arbeiten kann

    Was also sollte dieser Test zeigen/beweisen?

    Jeder kann bei einem Wettrennen gewinnen wenn er dem anderen vorher erstmal die Beine bricht 🙂

    und schön das ihr es am andere Ende mit einem Ozi gemessen habt - ändert nur leider nichts daran das ihr etwas optimiert habt was der Kompiler (richtig verwendet) besser gemacht hätte

    Oft wird einfach vergessen die Optimierung zu aktivieren (was absoluter Standard in der Branche ist) oder der Benchmark-Code macht nicht das was man denkt


Anmelden zum Antworten