Schnelligkeit von C-Code
-
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 explizitregister
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.
-
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.
-
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.
-
HansKlaus schrieb:
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.
Gast3 schrieb:
Ist das ein Troll-Versuch - oder ernst gemeint?
-
dachschaden schrieb:
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".ja der gcc unterstützt aber keine prozessoren mit 16 bit datenbreite (warum auch immer) und der openwatcom ist doch sonst eigentlich ziemlich gut, oder 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.mit scheint, dass compiler so allgemein ihre tücken haben können. muss ja irgendwie auch so sein, wenn es keine fehlerfreien programme geben kann.
Die korrekte Implementierung wäre natürlich gewesen, den Funktionsaufruf außerhalb der Schleife zu haben.
ja aber wer soll das machen? die wenigsten werden die zeit haben, erst einmal den compiler umzuschreiben und das dann auch noch ausgiebig zu testen.
andererseits wirds in 95% der fälle sowieso egal sein und in den restlichen 5% nimmt man halt einfach assembler.
-
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