Hallo, liebe Assembler-Freunde!
Ich soll im Rahmen eines Projektes versuchen, C-Code mittels
Inline-Assembler Code optimieren. Da vor allem Assembler neu für mich
ist (habe bereits einige Tutorials durchgearbeitet), habe ich jedoch
keine Ahnung, wo ich ansetzen soll, um den Code performanter zu machen.
Das Board, auf dem das ganze passieren soll, ist ein ArduinoMega2560,
der C-Code ist ein Cypher. Interessant wäre deshalb vor allem die
"Round-Function", also die Funktion, die oft auf den Text angewandt
wird, um ihn zu verschlüsseln, da diese bei einem
Verschlüsselungsvorgang mehrere hunderte Male angewandt wird und so
bereits kleine Performanzverbesserungen einen großen Ausschlag geben.
In dieser Funktion geht es hauptsächlich um viele Bitshifts/Rotationen
und XOR/OR-Operationen. Die Worte, die geshiftet und miteinander geXORt
werden, sind 16 Bit lang, belegen also jeweils zwei Register.
Meine Frage wäre also, ob ihr mir Tipps geben könntet, wo ich ansetzen
kann, oder ob es sowieso keinen Sinn macht zu versuchen, besser als der
Compiler zu sein.
Ich habe bereits gelesen, dass vor allem bei Multiplikationen
Performanzgewinne gegenüber C möglich sind (z. B. bei
http://stackoverflow.com/a/577856), jedoch finden in dem Cypher keine
Multiplikationen statt.
Eine weitere Frage wäre noch, ob es eine Möglichkeit gibt, wie ich an
den kompilierten avr-gcc Assembler-Code herankommen kann. Bisher habe
ich nur die Seite http://gcc.godbolt.org/ genutzt, die jedoch nicht die
aktuelle Version des avr-gcc anbietet.
Vielen Dank im Voraus,
Thomas
Wenn du Atmel Studio benutzt, dann sollte bereits eine *.lss Datei mit
dem generierten Assembler Code erstellt worden sein. Wenn du avr-gcc
direkt benutzt dann:
avr-objdump -h -S projekt.elf > projekt.lss
Ein guter Anfang ist auf jeden Fall sich den generierten Assembler Code
anzugucken und dann zu überlegen ob einem Stellen für Verbesserungen
einfallen.
Thomas W. schrieb:> Meine Frage wäre also, ob ihr mir Tipps geben könntet, wo ich ansetzen> kann
Miss die Ausführungszeiten der einzelnen Teile.
Am besten geht das, wenn man noch ein paar freie IO-Pins hat, mit
denen man „wackeln“ kann, wenn man bestimmte Codeteile betritt oder
verlässt. Am allerbesten natürlich, wenn man das dann auf einem
Logikanalysator aufzeichnen kann.
Wenn du erstmal weißt, ob deine Runden-Funktion wirklich der
Flaschenhals ist, dann kannst du überlegen, ob sich dort durch
manuelle Optimierung wirklich nennenswert was rausholen lässt.
Boris P. schrieb:> Falsches Forum...
Verschoben.
Thomas W. schrieb:> Ich soll im Rahmen eines Projektes versuchen, C-Code mittels> Inline-Assembler Code optimieren.
Das ist nur was für eingefleischte Masochisten.
Wenn es unbedingt Assembler sein muß, dann als separates Quellfile
(*.S).
Aber solange Du nicht der absolute Assembler-Geek bist, sehe ich da
wenig bis kaum Erfolgschancen.
Welcher Hirni vergibt bloß solche Aufgaben. Hast Du ihm was getan, daß
er so böse auf Dich ist.
Peter Dannegger schrieb:> Aber solange Du nicht der absolute Assembler-Geek bist, sehe ich da> wenig bis kaum Erfolgschancen.
Wo ich viel mehr Erfolgschancen sehe, das ist bei der konsequente
Durchsicht des C Codes. Oftmals ist es so, dass diejenigen, die nach
Assembler-Optimierung rufen, haarsträubende Probleme im C Code nicht
sehen. Das beginnt bei der Wahl der falschen Datentypen und geht bis zu
schlechten Algorithmen bzw. schlechter Umsetzung. Um mal die Phrase "Dem
Compiler Prügel zwischen die Beine zu werfen" nicht zu verwenden.
Grossartige Performance Gewinne sind durch Assemblerlösungen eigentlich
selten bis kaum zu erzielen. Dazu sind die Compiler schon zu gut.
Bitrotation in einem Byte ist eine der Schwachpunkte in C, weil es dafür
keine High-Level Entsprechung in C gibt, d.h. diese Absicht muss ein
Optimizer erkennen können. Aber ansonsten? Ob man 2 Werte in C
miteinander ver-xodert, oder ob man dieselbe Operation in Assembler
hinschreibt kommt aufs selbe raus.
Bevor man zu inline asm greift (und der C Code selbst bereits optimiert
ist), sollte man Compiler Intrinsics in Betracht ziehen.
Beispiel für armcc und Cortex-M3:
16Bit signed BE auf 32Bit signed LE (ansonsten per SHIFT, OR & MASK
sowie sign extension):
leVal = _REVSH(beVal);
Eine gute Methode ist auch, verschiedene Ansätze in C mit dem compilat
in ASM zu vergleichen, um zu schauen, was der compiler draus macht. Der
Gewinn wird ggf. sogar grösser als durch pure ASM Fummelei.
Schlussendlich sollte man Teile seines Programms ausmessen. Dies kann im
einfachsten Fall per Pin Toggling (über eine Code Section) geschehen,
oder bei grösseren µCs z.B. durch ein Virtuelles Oszilloskop in der IDE
mit einem schnellen target Debugger.
Random ... schrieb:> Eine gute Methode ist auch, verschiedene Ansätze in C mit dem compilat> in ASM zu vergleichen, um zu schauen, was der compiler draus macht.
Ja, die Optimierung auf C-Ebene ist deutlich einfacher.
Es reicht dazu, im Assembler-Listing zu zählen, wieviel Instruktionen
ein C-Ausdruck erzeugt.
Peter Dannegger schrieb:> Es reicht dazu, im Assembler-Listing zu zählen, wieviel Instruktionen> ein C-Ausdruck erzeugt.
Da der Optimierer umstrukturiert, kann man selten einzelne ASM Befehle
konkret einer C-Anweisung zuordnen.
> mehrere hunderte Male angewandt> Da der Optimierer umstrukturiert
Wenn der Compiler da mehrere hundert Funktionsaufrufe durch Inline
expansion optimiert, kann deine Assembler-optimierte Funktion insgesammt
zu langsameren Code führen. Wenn der Compiler den Code nicht selbst
erzeugt hat, optimiert er auch nicht.
Steffen Rose schrieb:> Da der Optimierer umstrukturiert, kann man selten einzelne ASM Befehle> konkret einer C-Anweisung zuordnen.
Wenn man das Inlining abschaltet, geht das recht gut. Zumindest beim
WINAVR2010. Die Quelltextkommentare sind meistens an der richtigen
Stelle oder in der Nähe.
Mit Inlining muß man eben an der Aufrufstelle suchen.
Auch der WinAVR2010 wird bei eingeschaltetem Optimierer
Registerzuweisungen nicht mehrfach ausführen u.a.
Da kann es dann vorkommen, dass manche C-Anweisung als wenig aufwändig
angenommen wird. Löscht man nun die davorliegende "aufwändige"
C-Anweisung, verschiebt sich das Befüllen der Register auf die nun nicht
mehr einfache Anweisung.
Bei -O0 kann man noch gut nachkommen, aber bei -Os muss man schon
genauer kucken, welche an anderer Stelle liegenden ASM Anweisung noch
dazugehören.
Wie sehr WinAVR umstrukturiert, weiß ich nicht. Da wirst Du sicher recht
haben.
Mit Lötkolben sind wir Hobby-Frickler besser als die Compiler-Bauer. Wir
konnen die Funktion in ein FPGA brennen - die Compiler-Bauer können das
nicht :-)
Es hängt sehr von der Aufgabe ab, ob man von Hand deutlich besser
optimieren kann als der Compiler. Dazu sollte man aber schon etwas Übung
in ASM haben. Als Anfänger tut man sich da oft recht schwer.
Gute Chancen hat man mit ASM deutlich schneller zu werden, wenn man in
ASM kleinere Datentypen nutzen kann, etwa 20 Bit Zahlen oder gemischte
Operationen wie 16 Bit mal 8 Bit Zahl, die die meisten C-Compiler nicht
unterstützen. Auch über die direkte Nutzung des Carry flags geht es
manchmal deutlich schneller als in C, weil man sich ggf. eine IF Else
sparen kann. Manchmal findet aber auch GCC schon das Optimum oder ist
dicht dran.
Zum optimieren gibt es 2 Stufen: einmal vom ASM Code ausgehen den der
Compiler erzeugt hat, und dann den zu Optimieren. Alternativ löst man
das Problem gleich in ASM, ohne Code vom Compiler als Vorbild - das gibt
ggf. ganz andere Ansätze wo z.B. IF ..ELSE Unterscheidungen wegfallen
und stattdessen Flag Bits direkt genutzt werden. Auch Tabellen sind ggf.
Effektiv.
Wie lange ein Stück Code wirklich braucht, kann man ganz gut im
Simulator testen: einfach vor und hinter den interessanten Teil
Breakpoints setzen. Das ist einfacher als von Hand Zyklen zählen.
Vielen Dank schon mal an alle für die vielen, schnellen Antworten! Hat
mir auf jeden Fall neue Ansätze gegeben.
Ist echt super, dass sich hier so schnell um Probleme gekümmert wird,
weiter so!