Schnelligkeit von C-Code
-
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
-
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:
#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.
-
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?
-
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.
-
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.
-
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?