Schnelligkeit von C-Code



  • Hallo,

    ich muss für einen Mikroprozessor ein Programm schreiben, in dem es zeitkritische Bereiche gibt. Der Mikroprozessor ist schon stark ausgelastet, da neben meinem Programm noch andere Berechnungen vorgenommen werden.

    Deswegen versuche ich meinen C-Code, in den zeitkritischen Bereichen, möglichst klein zu halten.

    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)?



  • Das hängt vom Prozessor ab. Normalerweise bedeutet das Allozieren von Variablen auf dem Stack gar nichts. Aber bis eine Line im Cache liegt, können etliche Cycles vergehen, und je schneller die CPU, desto mehr Leerlauf wird erzeugt. Weil so ein Bus echt langsam ist. Und das wird nicht besser, nur schlimmer in der Zukunft. Damals (1980s) haben wir noch ~10 Cycles für einen Speicherzugriff gebraucht, heute sind's so 200 (i7 Core). Und das rechnet noch nicht mal eventuelle TLB-Misses mit ein (weil wir halt auch nicht wissen, ob deine Architektur überhaupt Paging kann), die dann noch mal dazu führen können, dass im Speicher nach Adressübersetzungen gesucht werden muss.

    Was hast du denn für Möglichkeiten? Kannst du bspw. alloca verwenden? Das ist gut für kleinere, dynamische Allokationen, bei der du nicht den Morloch, den malloc - und free -Aufrufe beschwören, wieder bändigen musst. Und vor allen ruiniert es dir nicht den Prozessor-Cache, weil malloc intern Listen durchgehen muss und dann wieder eine Menge Blödsinn eingeladen werden könnte.

    Und wenn das nicht möglich ist, dann würde es vermutlich was bringen, malloc und free außerhalb des zeitkritischen Bereiches zu verwenden. Dann musst du halt mit persistenten Speicherblöcken arbeiten. Aber das kann es wert sein, wenn du wirklich hungrig nach jedem Cycle bist.

    Ansonsten: mehr Informationen.



  • Hallo dachschaden,

    es geht mir nicht darum Speicher während der Laufzeit bereit zu stellen.

    Folgendes:

    Ich habe eine Funktion parseTemp() welche mir einen HEX Wert in einen float Wert übersetzt.

    /*Funktion übersetzt den HEX Code, welchen der TMP006 liefert in eine Temperatur in °C
    	Übergeben wird ein Zeiger auf einen 2 Byte Speicherbereich. 
    	Fehlermeldungen:
    	-273.15	°C      Temperatur ist größer als 150 °C oder kleiner als 65 °C	(Messbereich des TMP006)
    */
    
    float parseTemp(short *Value_TMP006)	
    {
    	bool auswerter = false;								//Lege Boolvariable an, welche die Auswertung der einzelnen Stellen vornimmt.															
    	int j = 0x4;										//Lege 16 Bit an, welche zum Extrahieren des Auszuwertenden Bits benötigt werden.
    	const float abs_null = -273.15;						//Definiere Konstante, für Rückgabewert bei Fehler.
    
    	float temperatur = 0.0;	
        convert[14] = {0.03125, 0.0625, 0.125, 0.25, 0.5, 1.0, 2.0, 4.0, 8.0, 16.0, 32.0, 64.0, 128.0, -1.0};			
    
    /*[...]*/	
    }
    

    Ich habe den Array convert[14] bereits als globalen Array ausgelagert, da dieser nicht mehr verändert wird und nur dazu dient die Temperatur richtig zusammen zu addieren. Also wäre ein Anlegen dieses Arrays bei jedem Aufruf der Funktion nur mit unnötigen Rechenoperationen verbunden.
    Den const float Wert könnte ich auch als Symbolische Konstante anlegen, nur meckert dann der Compiler, dass er einen Double in einen Float konvertieren muss, was wiederum eine unnötige Rechenoperation wäre. (Den Wert würde ich auch noch auslagern)

    Generell würde mich jetzt interessieren, ob es vorteilhaft ist bei Funktionsaufrufen die Variablen schon vorher zu Deklarieren.

    Mein Prozessor ist übrigens ein 32-Bit RISC Prozessor der auf der ARMv7-M
    Harvard-Architektur basiert. (Ist eingebaut im Mikroprozessor: MK60DN256VLL10)



  • Erik12345679 schrieb:

    Ich habe den Array convert[14] bereits als globalen Array ausgelagert, da dieser nicht mehr verändert wird und nur dazu dient die Temperatur richtig zusammen zu addieren. Also wäre ein Anlegen dieses Arrays bei jedem Aufruf der Funktion nur mit unnötigen Rechenoperationen verbunden.

    Wenn sich das Array nicht ändert, kannst du es als static const deklarieren. Zumindest mein Compiler (gcc mit -O3) optimiert die Erstellung des Arrays dann weg, und du hast keine globalen Variablen oder ähnliches.

    Erik12345679 schrieb:

    Den const float Wert könnte ich auch als Symbolische Konstante anlegen, nur meckert dann der Compiler, dass er einen Double in einen Float konvertieren muss, was wiederum eine unnötige Rechenoperation wäre. (Den Wert würde ich auch noch auslagern)

    F-Suffix? -273.15f ?

    Erik12345679 schrieb:

    Generell würde mich jetzt interessieren, ob es vorteilhaft ist bei Funktionsaufrufen die Variablen schon vorher zu Deklarieren.

    Habe ich schon geschrieben, das Anlegen auf dem Stack ist praktisch nichts. Es sei denn, du arbeitest mit großen Arrays, die sich verändern können oder so.

    Wenn es sich um eine Funktion in dem gleichen Modul (Übersetzungseinheit) handelt wie die Funktion, die deine Funktion aufruft, oder wenn du Link-Time-Optimization aktiviert hast, dann kann der Compiler/der Linker entscheiden, es bei der Aufrufkonvention nicht ganz so ernst zu nehmen und ein paar Parameter oder Teile des Funktionsaufrufs wegzuoptimieren. Aber sicher muss das nicht sein.



  • Okay. Danke 🙂

    Woran sehe ich denn, ob der Compiler das weg optimiert?

    Wie kann ich denn meine Funktion noch "schnell machen"? Ist es besser die Funktion als void zu deklarieren und ihr einen Zeiger auf einen float zu übergeben, auf den sie den Temperaturwert schreibt?

    Hast du (oder jemand anderes) evtl. einen guten Link oder eine gute Buchempfehlung zur Optimierung von C-Code?

    Das was ich über Google bis jetzt gefunden habe beschränkt sich auf das:
    http://www.asdala.de/algorithmik/opt.html



  • Erik12345679 schrieb:

    Generell würde mich jetzt interessieren, ob es vorteilhaft ist bei Funktionsaufrufen die Variablen schon vorher zu Deklarieren.

    Du meinst Definitionen und mit "vorher Deklarieren" meinst du globale Variablen.
    Globale Variablen sind scheiße und generell nicht zu empfehlen.
    Globale Konstanten hingegen kann man verwenden:

    const float glbFloat = -273.15F;
    /* Beachte den type-suffix F */
    

    Literale sind dann nochmal was anderes:

    #define GLB_FLOAT -273.15F
    

    Theoretisch benötigt die Literal-Variante weniger Speicher, die Laufzeit des Programms hängt vom Compiler und/oder Hardware ab, da heißt es: messen,messen,messen (unter produktivnaher Umgebung)

    Generell sollten float-Operationen ggü. double-Operationen schneller (wenn auch weniger genau) ablaufen, deshalb solltest du ausschließlich mit explizitem float-Typ arbeiten (C99 hat dafür auch neue Funktionen, z.B. fabsf() statt fabs() ) und bei den Literalen auf den type-suffix (F oder f) achten (damit dein Compiler möglichst keinerlei double-Code erzeugt), z.B.

    statt
    float f = 1.23 * sqrt(9.0);
    besser
    float f = 1.23F * sqrtf(9.0F);
    


  • Erik12345679 schrieb:

    Woran sehe ich denn, ob der Compiler das weg optimiert?

    Ich weiß nicht, welches System du am Start hast, aber ich verwende objdump -d -Mintel auf Linux zum Anzeigen des Maschinencodes.

    #include <stdio.h>
    #include <stdint.h>
    
    int main(void)
    {
        float convert[14] = {0.03125, 0.0625, 0.125, 0.25, 0.5, 1.0, 2.0, 4.0, 8.0, 16.0, 32.0, 64.0, 128.0, -1.0};             
        size_t i;
    
        for(i = 0;i < 14;i++)
            printf("%f\n",convert[i]);
    
        return 0;
    }
    
    00000000004004f0 <main>:
      4004f0:       55                      push   rbp
      4004f1:       53                      push   rbx
      4004f2:       48 83 ec 48             sub    rsp,0x48
      4004f6:       64 48 8b 04 25 28 00    mov    rax,QWORD PTR fs:0x28
      4004fd:       00 00 
      4004ff:       48 89 44 24 38          mov    QWORD PTR [rsp+0x38],rax
      400504:       31 c0                   xor    eax,eax
      400506:       48 8d 6c 24 38          lea    rbp,[rsp+0x38]
      40050b:       c7 04 24 00 00 00 3d    mov    DWORD PTR [rsp],0x3d000000
      400512:       c7 44 24 04 00 00 80    mov    DWORD PTR [rsp+0x4],0x3d800000
      400519:       3d 
      40051a:       c7 44 24 08 00 00 00    mov    DWORD PTR [rsp+0x8],0x3e000000
      400521:       3e 
      400522:       c7 44 24 0c 00 00 80    mov    DWORD PTR [rsp+0xc],0x3e800000
      400529:       3e 
      40052a:       48 89 e3                mov    rbx,rsp
      40052d:       c7 44 24 10 00 00 00    mov    DWORD PTR [rsp+0x10],0x3f000000
      400534:       3f 
      400535:       c7 44 24 14 00 00 80    mov    DWORD PTR [rsp+0x14],0x3f800000
      40053c:       3f 
      40053d:       c7 44 24 18 00 00 00    mov    DWORD PTR [rsp+0x18],0x40000000
      400544:       40 
      400545:       c7 44 24 1c 00 00 80    mov    DWORD PTR [rsp+0x1c],0x40800000
      40054c:       40 
      40054d:       c7 44 24 20 00 00 00    mov    DWORD PTR [rsp+0x20],0x41000000
      400554:       41 
      400555:       c7 44 24 24 00 00 80    mov    DWORD PTR [rsp+0x24],0x41800000
      40055c:       41 
      40055d:       c7 44 24 28 00 00 00    mov    DWORD PTR [rsp+0x28],0x42000000
      400564:       42 
      400565:       c7 44 24 2c 00 00 80    mov    DWORD PTR [rsp+0x2c],0x42800000
      40056c:       42 
      40056d:       c7 44 24 30 00 00 00    mov    DWORD PTR [rsp+0x30],0x43000000
      400574:       43 
      400575:       c7 44 24 34 00 00 80    mov    DWORD PTR [rsp+0x34],0xbf800000
      40057c:       bf 
      40057d:       0f 1f 00                nop    DWORD PTR [rax]
      400580:       66 0f ef c0             pxor   xmm0,xmm0
      400584:       be 44 07 40 00          mov    esi,0x400744
      400589:       bf 01 00 00 00          mov    edi,0x1
      40058e:       b8 01 00 00 00          mov    eax,0x1
      400593:       48 83 c3 04             add    rbx,0x4
      400597:       f3 0f 5a 43 fc          cvtss2sd xmm0,DWORD PTR [rbx-0x4]
      40059c:       e8 3f ff ff ff          call   4004e0 <__printf_chk@plt>
      4005a1:       48 39 eb                cmp    rbx,rbp
      4005a4:       75 da                   jne    400580 <main+0x90>
      4005a6:       31 c0                   xor    eax,eax
      4005a8:       48 8b 54 24 38          mov    rdx,QWORD PTR [rsp+0x38]
      4005ad:       64 48 33 14 25 28 00    xor    rdx,QWORD PTR fs:0x28
      4005b4:       00 00 
      4005b6:       75 07                   jne    4005bf <main+0xcf>
      4005b8:       48 83 c4 48             add    rsp,0x48
      4005bc:       5b                      pop    rbx
      4005bd:       5d                      pop    rbp
      4005be:       c3                      ret    
      4005bf:       e8 ec fe ff ff          call   4004b0 <__stack_chk_fail@plt>
      4005c4:       66 2e 0f 1f 84 00 00    nop    WORD PTR cs:[rax+rax*1+0x0]
      4005cb:       00 00 00 
      4005ce:       66 90                   xchg   ax,ax
    

    Da sieht man deutlich die Erstellung des Arrays auf dem Stack. Jetzt fügt man das static const hinzu und kompiliert neu:

    0000000000400480 <main>:
      400480:       53                      push   rbx
      400481:       31 db                   xor    ebx,ebx
      400483:       0f 1f 44 00 00          nop    DWORD PTR [rax+rax*1+0x0]
      400488:       66 0f ef c0             pxor   xmm0,xmm0
      40048c:       be 44 06 40 00          mov    esi,0x400644
      400491:       bf 01 00 00 00          mov    edi,0x1
      400496:       b8 01 00 00 00          mov    eax,0x1
      40049b:       f3 0f 5a 04 9d 60 06    cvtss2sd xmm0,DWORD PTR [rbx*4+0x400660]
      4004a2:       40 00 
      4004a4:       48 83 c3 01             add    rbx,0x1
      4004a8:       e8 c3 ff ff ff          call   400470 <__printf_chk@plt>
      4004ad:       48 83 fb 0e             cmp    rbx,0xe
      4004b1:       75 d5                   jne    400488 <main+0x8>
      4004b3:       31 c0                   xor    eax,eax
      4004b5:       5b                      pop    rbx
      4004b6:       c3                      ret    
      4004b7:       66 0f 1f 84 00 00 00    nop    WORD PTR [rax+rax*1+0x0]
      4004be:       00 00
    

    Und plötzlich ist die Erstellung des Arrays weg.

    Erik12345679 schrieb:

    Wie kann ich denn meine Funktion noch "schnell machen"? Ist es besser die Funktion als void zu deklarieren und ihr einen Zeiger auf einen float zu übergeben, auf den sie den Temperaturwert schreibt?

    Damit sparst du dir eine Kopie (anstatt dass der Returnwert erst auf den Stack und nach der Funktion in die Variable geschrieben wird, wird er direkt in die Variable geschrieben). Aber auch hier gilt: kann wegoptimiert werden, wenn der Compiler Caller und Callee gerade griffbereit hat. Die hat er aber nur griffbereit, wenn es sich um eine Übersetzungseinheit handelt. Ansonsten muss der Linker ran. Und seitdem der gcc memset auf lokale Variablen wegwirft, vertraue ich LTO nicht mehr so ganz.



  • abhängig vom compiler und vom programm selbst werden bei aufrufen von unterprogrammen auch gerne mal die daten auf den stack geschoben und danach wieder heraufgeholt, was auch wieder zeit frisst.

    inline-assembler bringt auch sehr oft zeitvorteile, genauso wie die verwendung des schlüsselworts register.



  • abhängig vom compiler und vom programm selbst werden bei aufrufen von unterprogrammen auch gerne mal die daten auf den stack geschoben und danach wieder heraufgeholt, was auch wieder zeit frisst.

    inline-assembler bringt auch sehr oft zeitvorteile, genauso wie die verwendung des schlüsselworts register.

    Ist das ein Troll-Versuch - oder ernst gemeint?



  • natürlich ist das ernst gemeint.



  • Das register -Schlüsselwort bringt nicht wirklich viel, weil Compiler heutzutage schlau genug sind, zu erkennen, welche Variablen in den Registern bleiben können und welche im Hauptspeicher bleiben müssen. Ich habe vereinzelt Variablen explizit register zuweisen müssen, aber dann ging es meist um Emulations- oder anderen hochoptimierten Code.

    Als Anfänger packt man dann überall register rein, denkt sich, man ist voll schlau, aber es bringt nicht wirklich was.

    Außerdem hat der TE eine RISC-Architektur. Die zeichnen sich in der Regel durch viele, viele Register aus, und das weiß der Compiler auch.



  • und darauf kann man sich verlassen?

    was ist mit inline-assembler? ich habe dazu gelernt, dass assembler gewissermaßen direkt auf die funktionen der cpu zugreift bzw. unmittelbar in opcodes umgewandelt werden kann und daher immer schneller ist, als der compiler es hinbekommt, wenn man jetzt so späße wie mehrfache addition statt multiplikation und derartiges vermeidet.


  • Mod

    HansKlaus schrieb:

    und darauf kann man sich verlassen?

    was ist mit inline-assembler? ich habe dazu gelernt, dass assembler gewissermaßen direkt auf die funktionen der cpu zugreift bzw. unmittelbar in opcodes umgewandelt werden kann und daher immer schneller ist, als der compiler es hinbekommt, wenn man jetzt so späße wie mehrfache addition statt multiplikation und derartiges vermeidet.

    Häh? Was meinst du denn, was der Compiler erzeugt, wenn nicht das? C ist nicht Java, da gibt es keine Zwischenschritte zwischen Programm und CPU. 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.



  • naja ich frag deshalb so bescheuert, weil wir als laboraufgabe jeweils mit outp aus der conio.h und mit inline-assembler und out ein rechtecksignal erzeugen und dann erklären sollten, wieso das signal mit c irgendwo bei 150kHz und mit inline-assembler bei 270 kHz liegt und die botschaft war eben, dass der compiler das nicht so vernünftig hinbekommt und dass zeitkritische funktionen dann in (inline-) assembler zu realisieren sind.



  • HansKlaus schrieb:

    naja ich frag deshalb so bescheuert, weil wir als laboraufgabe jeweils mit outp aus der conio.h und mit inline-assembler und out ein rechtecksignal erzeugen und dann erklären sollten, wieso das signal mit c irgendwo bei 150kHz und mit inline-assembler bei 270 kHz liegt und die botschaft war eben, dass der compiler das nicht so vernünftig hinbekommt und dass zeitkritische funktionen dann in (inline-) assembler zu realisieren sind.

    So ein Problem hatte ich vor 20 Jahren auch mal.
    Allerdings war der C-Compiler so gut, dass ich es nicht (deutlich genug) schneller bekommen habe.
    Allerdings war das auch auf einem Atari ST.



  • HansKlaus schrieb:

    und darauf kann man sich verlassen?

    Nein, kann man nicht. Deswegen wird dir auch jeder, den du bezüglich deines Algorithmus fragst, zumindest nahelegen, selbst zu testen, was schneller ist. Wenn es um zeitkritische Aufgaben geht, ist der Messaufwand gerechtfertigt.

    HansKlaus schrieb:

    naja ich frag deshalb so bescheuert, weil wir als laboraufgabe jeweils mit outp aus der conio.h und mit inline-assembler und out ein rechtecksignal erzeugen und dann erklären sollten, wieso das signal mit c irgendwo bei 150kHz und mit inline-assembler bei 270 kHz liegt

    Und hast du dir den generierten Maschinencode angeschaut und festgestellt, warum der Compiler so langsamen Code generiert? Es gibt auch das Phänomen, dass Compiler halt nicht schlau genug sind, um zu optimieren.

    Wobei ich erst einmal frech behaupten würde, dass die meiste Zeit eh in outp verbracht wird.

    Mich würde der C-Code, der erzeugte Maschinencode, und das schnellere Inline-Assembly interessieren, damit ich einen direkten Vergleich habe und nicht raten müsste, warum das Assembly schneller ist.



  • Auf jeden Fall sollte man sich ruhig mal anschauen, was der C Compiler an Assemblercode generiert. Zu 486er Zeiten gab es da tatsächlich noch Unterschiede zwischen

    int a=4,b;
    b = a / 2;
    

    und

    int a=4,b;
    b = a >> 1;
    

    Das bekommen heutige Compiler aber problemlos hin optimiert. 🙂


  • Mod

    Burkhi schrieb:

    Auf jeden Fall sollte man sich ruhig mal anschauen, was der C Compiler an Assemblercode generiert. Zu 486er Zeiten gab es da tatsächlich noch Unterschiede zwischen

    int a=4,b;
    b = a / 2;
    

    und

    int a=4,b;
    b = a >> 1;
    

    Das bekommen heutige Compiler aber problemlos hin optimiert. 🙂

    sogar für 486er.



  • also das programm lautete einmal

    int main()
    {
    //Hier noch bisschen Initialisierung
    
    for(;;)
    {
    outp(PORT,0x01);
    outp(PORT,0x00);
    }
    }
    

    und einmal

    int main()
    {
    //Initialisierung
    
    for(;;)
    {
    _asm 
    { 
    mov al,0x01 
    mov dx,PORT 
    out dx,al
    mov al,0x00 
    mov dx,LPT 
    out dx,al 
    } 
    }
    
    }
    

    wurde beides ganz normal mit dem openwatcom-compiler (den ich natürlich gerade nicht auf dem rechner habe) gebaut. letztenendes unterschieden sich die jeweiligen assemblercodes nur darin, dass in dem c-programm jedes mal eine funktion/unterprogramm namens check oder so aufgerufen wurde und man sagte mir, dass der compiler dieses check dazu benutzt, um überhaupt die richtigen einstellungen zu finden.

    ich hab eben noch einmal nachgesehen, die frequenzen waren 117kHz und 320kHz, also fast das dreifache an unterschied. 🙄



  • HansKlaus schrieb:

    letztenendes unterschieden sich die jeweiligen assemblercodes nur darin, dass in dem c-programm jedes mal eine funktion/unterprogramm namens check oder so aufgerufen wurde und man sagte mir, dass der compiler dieses check dazu benutzt, um überhaupt die richtigen einstellungen zu finden.

    Wenn ich das richtig verstehe, war also das Problem, dass ein Funktionsaufruf, der auch außerhalb der Schleife hätte stehen können, nicht wegoptimiert wurde. Der GCC kennt solche Funktionen als pure functions , die also keine Seiteneffekte besitzen und das Ergebnis nur vom Input abhängt - die also "auch mal weniger oft als geschrieben aufgerufen werden können".

    Ob der Watcom das unterstützt, weiß ich nicht. Aber meines Wissens hat auch der Visual Studio Compiler z.B. Probleme damit, strlen -Calls auf konstante Strings einfach mit der String-Länge zu ersetzen.

    Die korrekte Implementierung wäre natürlich gewesen, den Funktionsaufruf außerhalb der Schleife zu haben. 🙄


Anmelden zum Antworten