Schnelligkeit von C-Code



  • Ist das ein Troll-Versuch - oder ernst gemeint?

    wenn jemand so nebenbei von register und inline-Assembler anfängt dann ist es schon sehr relevant welcher Kompiler und was für ein Problem optimiert wurde - hier ist es ein alter (zugegeben damals sehr guter DOS C Compiler) und ein absolut triviales IO Beispiel - das ist in dem Kontext eines neueren Kompilers und so allgemein als "schneller" formuliert einfach zu plump - daher die Troll Frage - aber jetzt ist ja klar wo das Wissen/Erfahrung herkommt - hier für diesen Fall unrelevant



  • @HansKlaus - welche Version von Watcom C war das - ich würde mal probieren das ganze mit dem Build von https://github.com/open-watcom (Version 2.0 beta Aug 26 2016 05:40:13 (32-bit)) zu kompilieren - dann kann man direkt vergleichen was der Unterschied zwischen C und inline-Assembler ist



  • so gehts:

    von https://github.com/open-watcom

    den nightly build runterladen

    https://github.com/open-watcom/travis-ci-ow-builds/archive/master.zip

    entpackend

    dann das Beispiel von HansKlaus:

    outp_test.c

    #include <conio.h>
    
    #define PORT 123
    
    int main1()
    {
        //Hier noch bisschen Initialisierung
        for(;;)
        {
            outp(PORT,0x01);
            outp(PORT,0x00);
        }
    }
    
    int main2()
    {
        //Initialisierung
        for(;;)
        {
            _asm
            {
                mov al,0x01
                mov dx,PORT
                out dx,al
                mov al,0x00
                mov dx,PORT
                out dx,al
            }
        }
    }
    
    int main()
    {
        main1();
        main2();
        return 0;
    }
    

    und diese Batch-Datei

    outp_test.cmd

    set WATCOM=C:\Users\dl\Downloads\travis-ci-ow-builds-master\travis-ci-ow-builds-master\rel
    set PATH=%WATCOM%\binnt;%PATH%;
    set INCLUDE=%WATCOM%\h
    set EDPATH=%WATCOM%\EDDAT
    set WIPFC=%WATCOM%\WIPFC
    wcl -q -bt=DOS d:\temp\outp_test.c
    

    und des Rätsels Lösung ist - kein inlineing von den outp-Funktionen
    (oder mir fehlen noch Optimierschalter beim kompilieren)

    seg000:0008 main1           proc near               ; CODE XREF: sub_1003B+6p
    seg000:0008                 mov     ax, 4
    seg000:000B                 call    loc_1004F
    seg000:000E                 push    dx
    seg000:000F
    seg000:000F loc_1000F:                              ; CODE XREF: main1+18j
    seg000:000F                 mov     dx, 1
    seg000:0012                 mov     ax, 7Bh ; '{'
    seg000:0015                 call    sub_10081 ; <=======
    seg000:0018                 xor     dx, dx
    seg000:001A                 mov     ax, 7Bh ; '{'
    seg000:001D                 call    sub_10081 ; <=======
    seg000:0020                 jmp     short loc_1000F
    seg000:0020 main1           endp
    seg000:0020
    seg000:0022
    seg000:0022 ; =============== S U B R O U T I N E =======================================
    seg000:0022
    seg000:0022 ; Attributes: noreturn
    seg000:0022
    seg000:0022 main2           proc near               ; CODE XREF: seg000:0044p
    seg000:0022                 mov     ax, 0Ch
    seg000:0025                 call    loc_1004F
    seg000:0028                 push    bx
    seg000:0029                 push    cx
    seg000:002A                 push    dx
    seg000:002B                 push    si
    seg000:002C                 push    di
    seg000:002D
    seg000:002D loc_1002D:                              ; CODE XREF: main2+17j
    seg000:002D                 mov     al, 1
    seg000:002F                 mov     dx, 7Bh ; '{'
    seg000:0032                 out     dx, al
    seg000:0033                 mov     al, 0
    seg000:0035                 mov     dx, 7Bh ; '{'
    seg000:0038                 out     dx, al
    seg000:0039                 jmp     short loc_1002D
    seg000:0039 main2           endp
    


  • und wenn nicht doch noch Optimierschalter fehlen ist der generierte Code in beiden Fällen semi-optimal - warum wird z.B. dx staendig ueberschrieben?



  • Erik12345679 schrieb:

    Meine Frage wäre nun: Bringt es einen Geschwindigkeitsvorteil, wenn ich meine Variablen außerhalb der zeitkritischen Bereiche anlege (z.B. beim Starten des µP)?

    Generell sind Alignment (auch Datentypkontrolle, -repräsentation oder -wechsel), Jumps verhindern und Abhängigkeiten auflösen (in den Asm-Code gucken und/oder in Schleifen) hilfreich.
    Wenn kein netter Algo greifbar ist, ist vielleicht eine nette Hardwarehilfe beim Prozessor nutzbar.
    Beim letzten wäre zu prüfen, was der Compiler optimieren kann (z.B. Konvertierungen, besondere Befehle oder Parallelgedöns) und was nicht.

    Internetseiten zum Thema Optimierung sollten möglichst aktuell sein und auch Prozessor- und Entwicklungssystem bezogen.
    Diese Seite hier ist noch nicht so alt, beschreibt aber schon weitere wichtige Grundregeln und Hilfen:
    http://stackoverflow.com/questions/10800372/c-tips-for-code-optimization-on-arm-devices



  • im Vergleich zu Watcom generieren der gcc 6.2 oder clang 3.8 (gcc.godbolt.org) dafür schon viel besseren Code - auch ohne (bis auf den outp-Ersatz) inline-asm

    #include <stdint.h>
    
    #define PORT 123
    
    static inline void outp(uint16_t port, uint8_t val)
    {
      asm ( "outb %0, %1" : : "a"(val), "Nd"(port) );
    }
    
    int main()
    {
      //Hier noch bisschen Initialisierung 
      for(;;) 
      { 
        outp(PORT,0x01); 
        outp(PORT,0x00); 
      } 
    
      return 0;
    }
    

    gcc 6.2

    main:
            mov     ecx, 1
            xor     edx, edx
    .L2:
            mov     eax, ecx
            outb al, 123
            mov     eax, edx
            outb al, 123
            jmp     .L2
    

    clang 3.8

    main:                                   
    .LBB0_1:                                
            mov     al, 1
            out     123, al
            xor     eax, eax
            out     123, al
            jmp     .LBB0_1
    

    so viel zu alt/neu Kompiler-Vergleiche



  • mit besseren Optimierflags für den Watcom

    wcl -q -bt=DOS /oneatx /oh /ei /zp8 /6 /fp6 d:\temp\outp_test.c

    ergibt sich ein völlig anderes Bild

    seg000:0010 main1           proc near               ; CODE XREF: sub_10050+5p
    seg000:0010                 push    bx
    seg000:0011                 push    dx
    seg000:0012                 mov     bx, 7Bh ; '{'
    seg000:0015                 xor     ah, ah
    seg000:0017                 mov     dx, bx
    seg000:0019
    seg000:0019 loc_10019:                              ; CODE XREF: main1+Fj
    seg000:0019                 mov     al, 1
    seg000:001B                 out     dx, al
    seg000:001C                 mov     al, ah
    seg000:001E                 out     dx, al
    seg000:001F                 jmp     short loc_10019
    seg000:001F main1           endp
    
    seg000:002F main2           proc near
    seg000:002F                 cld
    seg000:0030                 push    bx
    seg000:0031                 push    cx
    seg000:0032                 push    dx
    seg000:0033                 push    si
    seg000:0034                 push    di
    seg000:0035
    seg000:0035 loc_10035:                              ; CODE XREF: main2+12j
    seg000:0035                 mov     al, 1
    seg000:0037                 mov     dx, 7Bh ; '{'
    seg000:003A                 out     dx, al
    seg000:003B                 mov     al, 0
    seg000:003D                 mov     dx, 7Bh ; '{'
    seg000:0040                 out     dx, al
    seg000:0041                 jmp     short loc_10035
    seg000:0041 main2           endp
    

    d.h. der inline-Assembler-Code (main2) ist definitiv langsamer als der C-Code
    könnte an einer älteren Watcom-Version liegen - aber bei diesem Beispiel zeigt sich ganz deutlich warum Inline-Assembler immer mit Vorsicht zu genießen ist - und Aussagen die dazu getroffen werden



  • SeppJ schrieb:

    Und da der Compiler mit seinen tausenden Optimierungstricks in aller Regel viel besseren Maschinencode programmieren kann als der erfahrendste Assemblerhacker, erzeugt er in aller Regel auch viel schnelleren Code. Die einzige nennenswerte Ausnahme sind dabei allerneueste CPU-Features, die älteren Compilern einfach noch nicht gut bekannt sind.

    Das mag im Kontext von x64 und wahrscheinlich auch ARM so sein, aber die Welt ist größer und die allgemeine Annahme der superhochoptimierenden Compiler ist so pauschalisiert schlicht falsch.



  • 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.


Anmelden zum Antworten