Hallo,
ich habe mir interessehalber mal den Assembler Code angeschaut, den ich
vom AVR Studio 6.2 mit AVR GCC 4.8.1 produziert bekomme.
Dabei ist mir aufgefallen, dass einfache Bitmanipulationen, bei denen
zwei Bits im gleichen Register gesetzt oder gelöscht werden, nicht
optimiert werden.
Aus folgendem C-Code:
1
TIMSK|=(1<<TICIE1)|(1<<TOIE1);
Wird dann:
1
IN R24,0x39 In from I/O location
2
ORI R24,0x24 Logical OR with immediate
3
OUT 0x39,R24 Out to I/O location
Das braucht 3 Takte.
Warum wird das nicht zu
1
SBI 0x39,5
2
SBI 0x39,3
Das würde nur 2 Takte brauchen und somit 1/3 der Rechenzeit einsparen!
Habe es auch schon auf Optimierungsstufe -O0 bis -O2 versucht, immer mit
dem selben Ergebnis.
Kann mir jemand erklären, wie ich den GCC dazu bringen kann, das
vernünftig zu Optimieren?
Philipp
Philipp schrieb:> TIMSK |= (1<<TICIE1)|(1<<TOIE1);
Ist semantisch nicht gleich
TIMSK |= 1 << TICIE1;
TIMSK |= 1 << TOIE1;
Letzterem entspräche dein gewünschter Code.
Du hast mit deiner Zeile aber explizit verlangt, dass beide Bits
zeitgleich gesetzt werden, was je nach Register ein wesentlicher
Unterschied sein kann. Daher darf der Compiler das nicht zu einem
sequenziellen Setzen umwandeln.
Edit: bei einer nicht-volatile Variablen wäre das AFAIK eine mögliche
Optimierung, aber Nagel mich nicht darauf fest.
sbi kann nicht auf alle Register angewendet werden.
Wie war das noch mal? Alles was kleiner 32/0x20 ist?
Bei grossen Controllern fallen noch nicht mal alle
Portregister in diesen Bereich.
Unglaublich!
Dann gibt es also keine andere Möglichkeit als das Ganze in zwei Zeilen
zu schreiben?
SBI ist auf jeden Fall möglich, denn wenn ich nur ein Bit setzen/löschen
will, benutzt er tatsächlich diesen Befehl.
Philipp schrieb:> Unglaublich!
Was ist daran unglaublich?
Der Compiler darf das gar nicht anders machen!
Überleg mal, wenn es tatsächlich wichtig ist, dass die beiden Bits
gleichzeitig gesetzt werden MÜSSEN, weil das zb Portbits sind und da
etwas lebenswichtiges drann hängt, wenn die nicht gleichzeitig schalten.
Und dann kommt nach dem ersten sbi ein Interrupt dazwischen ....
1
sbi ...
2
<------------- hier kommt ein Interrupt
3
sbi ...
... und der Prozessor vertschüsst sich noch vor dem zweiten sbi in eine
ISR?
Na. Dämmerts, warum der Compiler nicht eigenmächtig die angeordnete
gleichzeitige Bitmanipulation einfach in 2 aufeinanderfolgende Schritte
aufdröseln darf?
Ok, überzeugt.
Dann werde ich wohl jedes bit mit einem eigenen Befehl setzen müssen.
Danke für die hilfreichen Antworten!
In der Zwischenzeit ist nochmal eine Ungereimtheit aufgetaucht, die zum
Thema passt:
Ich verwende in meinem Code sehr oft die Variable Zahnzaehler. Deswegen
habe ich diese mit der Zuweisung
1
registeruint8_tZahnzaehlerasm("r3");
an das Register r3 gebunden, damit sie nicht immer wieder erst aus dem
Speicher geladen werden muss.
An einer Stelle, wo ich die Variable nur um den Wert 1 Inkrementieren
will, produziert mir der Compiler aus
1
Zahnzaehler++
diese drei Zeilen hier:
1
LDI R20,0x01 Load immediate
2
ADD R20,R3 Add without carry
3
MOV R3,R20 Copy register
An einer anderen Stelle im Code, an der genau der gleiche Befehl steht,
macht er hingegen wie er soll
1
INC R3
An erstgenannter Stelle vergleiche ich die Variable direkt anschließend
noch mit einer Konstante, könnte das hier ausschlaggebend sein? Hier zur
Übersicht nochmal die ganze Stelle:
1
Zahnzaehler++;
2
LDIR20,0x01Loadimmediate
3
ADDR20,R3Addwithoutcarry
4
MOVR3,R20Copyregister
5
if(Zahnzaehler==15){
6
CPIR20,0x0FComparewithimmediate
7
BRNEPC+0x05Branchifnotequal
Selbst wenn man für den Vergleich den Wert von Zahnzaehler in Register
R20 bräuchte, wäre doch INC R3 gefolgt von MOV R20,R3 schneller.
Erwarte ich vielleicht einfach zu viel von einem Compiler?
Philipp schrieb:> register uint8_t Zahnzaehler asm ("r3");
kannst du überhaupt sicherstellen das r3 nicht in irgendeiner lib
verwendet wird? Wenn nein, lasst das mit dem Register lieber bleiben.
> Erwarte ich vielleicht einfach zu viel von einem Compiler?
vermutlich, er optimiert ausreichend gut, das heißt aber nicht das es
optimal ist.
Wenn dir der code zu langsam ist, dann schreibe die notwendigen
Funktionen direkt in ASM. So legst dem Compiler nur steine in den weg.
Solange du nicht absolut sicher bist, dass r3 frei ist und dass du
unbedingt dieses Register brauchst, solltest du dem Compiler da gar
nicht erst reinpfuschen. register ohne Festlegung reicht auch. In aller
Regel reicht es auch, das gar nicht zu machen, der Compiler wird
normalerweise schon in Register legen, was dort sinnvoll aufgehoben ist.
Hast du ein akutes Performance-Problem? Dann suche zunächst nach
größeren Schnitzern als einzelne Cycles zu tunen. Oder willst du nur
allgemein verstehen, was da an Code generiert wird? Dann mach weiter
aber bedenke, dass der Compiler einerseits nie so gut wie du wissen
kann, was du willst. Andererseits ist er im Zweifelsfall besser als du
daran, die entscheidenden Stellen zu optimieren.
Dass R3 nicht anderweitig gebraucht wird, habe ich vorher überprüft,
habe dazu den ASM Code nach R3 abgesucht, ist nirgendwo vorgekommen.
Dass der Compiler Variablen in Register legt, so sie frei sind, hätte
ich auch erwartet. Wäre ja nicht schwer, automatisch zu prüfen, welche
Register dauerhaft frei sind. Leider sind das aber tatsächlich einige
Register, die dauerhaft ungenutzt sind.
Ein Performance "Problem" habe ich noch nicht wirklich, aber ich
programmiere gerade an einer Art Motorsteuergerät, das über ein 60 Zahn
Geberrad syncronisiert ist.
Dort ist es besonders wichtig, dass Steuersignale (z.B. für Zündung oder
Einspritzung) möglichst schnell bzw. zeitnah an einem Ereignis erzeugt
werden. Deswegen ist es wichtig, dass die Anzahl der Takte bis zum
Schalten des Ports möglichst klein bleibt. Hier und da mal ein paar
Takte mehr können bei hohen Drehzahlen den Winkelfehler schnell
vergrößern.
Die zeitkritischen Steuerroutinen in ASM zu schreiben habe ich mir auch
schon überlegt. Leider habe ich keine Ahnung, wie man den Assemblercode
so schreibt, dass er sich nicht mit dem C-Code beißt.
Philipp schrieb:> Dass R3 nicht anderweitig gebraucht wird, habe ich vorher überprüft,> habe dazu den ASM Code nach R3 abgesucht, ist nirgendwo vorgekommen.
und machst du das jedes mal, wenn du eine neue Version vom Compiler
einspielst oder eine zusätzliche Funktion verwendest die du bis jetzt
noch nicht hattest?
> Die zeitkritischen Steuerroutinen in ASM zu schreiben habe ich mir auch> schon überlegt. Leider habe ich keine Ahnung, wie man den Assemblercode> so schreibt, dass er sich nicht mit dem C-Code beißt.http://www.mikrocontroller.net/articles/AVR-GCC-Tutorial/Assembler_und_Inline-Assembler
Philipp schrieb:> Dort ist es besonders wichtig, dass Steuersignale (z.B. für Zündung oder> Einspritzung) möglichst schnell bzw. zeitnah an einem Ereignis erzeugt> werden. Deswegen ist es wichtig, dass die Anzahl der Takte bis zum> Schalten des Ports möglichst klein bleibt. Hier und da mal ein paar> Takte mehr können bei hohen Drehzahlen den Winkelfehler schnell> vergrößern.
Timing durch abgezählte Prozessorzyklen erzeugen zu wollen, ist doch von
Anfang an ein Designfehler. Das hat man mal zu Zeiten des IBM-XTs
versucht und es ist gründlich in die Hose gegangen.
MfG Klaus
OMG!
https://www.mikrocontroller.net/articles/AVR-GCC-Codeoptimierung#Prinzipien_der_Optimierung
Das gilt nicht nur für Software, auch für so ziemlich alles anders.
Mal eine Überlegung. Bei 10 MHz dauert ein Takt = 1 ASM Befehl beim AVR
100ns. Bei 10.000 U/min dauert eine Umdrehung 36ms. 100ns/36ms = 2,7ppm
oder 0,001 Grad. Wenn nun durch nicht perfekte Programmierung rein in C,
OHNE das Schlüsselwort "register" VIELLEICHT eine Verzögerung oder
Jitter von 10 Takten zusätzlich reinkommt, macht das sagenhafte 0,01
Grad Winkelfehler. Ob dadurch ein Formel 1 Rennen verloren wird?
P S Schlaue Leute haben mit dem AVR schon vor Jahren eine jitterfreie
Videoausgabe gemacht, sogar in C. Der Trick ist wie immer Know How
(Sleep Mode, dadurch jitterfreier Einsprung in die ISR).
Philipp schrieb:> Dass R3 nicht anderweitig gebraucht wird, habe ich vorher überprüft,> habe dazu den ASM Code nach R3 abgesucht, ist nirgendwo vorgekommen.
Soso, du Kompilierst also immer 2x. Erst mal ohne Register Festlegung
und
prüfst dann ob R3 frei ist. Wenn ja dann das ganze nochmal mit register
Festlegung. Und du checkst den gesamten ASM Code und nicht nur die
parr Zeilen von main.c.
Um wie viele Bytes wird das denn dann kürzer?
> Dass der Compiler Variablen in Register legt, so sie frei sind, hätte> ich auch erwartet. Wäre ja nicht schwer, automatisch zu prüfen, welche> Register dauerhaft frei sind. Leider sind das aber tatsächlich einige> Register, die dauerhaft ungenutzt sind.
Die GCC Leute Freuen sich bestimmt über eine guten Patch.
> Dort ist es besonders wichtig, dass Steuersignale (z.B. für Zündung oder> Einspritzung) möglichst schnell bzw. zeitnah an einem Ereignis erzeugt> werden. Deswegen ist es wichtig, dass die Anzahl der Takte bis zum> Schalten des Ports möglichst klein bleibt. Hier und da mal ein paar> Takte mehr können bei hohen Drehzahlen den Winkelfehler schnell> vergrößern.
Um bei Falk's (dem ich nur zustimmen kann) 10.000 U/min zu Bleiben:
Das sind ja gerade mal 10Khz. Und zwischen 2 Zähnen sind das
dann 1000 Clocks. Für den AVR dreht sich der Motor in Super Zeitlupe.
Außerdem: Was glaubst du wie schnell ein Einspritzventil ist und wie
genau die Auswertung von dem Geberrad....
Achte besser auf fehlerfreien Code als auf 0,irgendwas µS und
vertraue deinem Compiler. Der macht das schon richtig.
@ tim (Gast)
>Um bei Falk's (dem ich nur zustimmen kann) 10.000 U/min zu Bleiben:>Das sind ja gerade mal 10Khz.
U/min! Nicht U/s! Das ist keine Hyperturbine ;-)
> Und zwischen 2 Zähnen sind das>dann 1000 Clocks. Für den AVR dreht sich der Motor in Super Zeitlupe.
Das wollte ich damit ausdrücken.
>Achte besser auf fehlerfreien Code als auf 0,irgendwas µS und>vertraue deinem Compiler. Der macht das schon richtig.
In GCC we trust!
;-)
P S Ich hab mich verrechnet, hab die 10.000 U/m durch 360 geteilt, war
wohl schon ne Rechnung weiter. Naja, macht trotzdem nur 6ms/U und damit
0,06 Grad Winkelfehler bei 10 Takten.
Falk B. schrieb:>> U/min! Nicht U/s! Das ist keine Hyperturbine ;-)
Die 10Kz Bezogen sich auf die Impulse vom Geberrad:
10.000U/min / 60 Sek * 60 Zähne
> In GCC we trust!
Yeah!
Bis jetzt hat er immer recht behalten.
Und um "Besseren" ASM-Code zu Schreiben
muss man sich schon verdammt anstrengen.
Das das hält man aber nicht das ganze
Projekt durch womit unterm Strich er dann
wieder "Besser" ist.
Ich mache diese Assembler-Dinge gar nicht mehr.
Weil in 2 Jahre kommt eine leicht geänderte Platine, oder das
Prozessor-Nachfolgemodell, oder nur ein neuer GCC.
Und schon funktioniert das Neu-Kompilieren nicht, weil irgendwas beim
Assembler nicht mehr stimmt. Dann debuggt man mühselig und ärgert sich
über den Blödsinn von damals.
Ich bleibe auf C-Ebene, das macht die Wartung und Pflege viel einfacher.
Und wenn die Leistung nicht reicht, dann muss eben eine schnellere CPU
benutzt werden. Dann würde aber auch bei Assembler die Leistung knapp
werden, und Programmerweiterungen sind dann auch bald nicht mehr
möglich.
PittyJ schrieb:> Ich mache diese Assembler-Dinge gar nicht mehr.> Weil in 2 Jahre kommt eine leicht geänderte Platine, oder das> Prozessor-Nachfolgemodell, oder nur ein neuer GCC.
Ein neuer GCC "kommt" nicht einfach, sondern auf den steigt man bewußt
um.
> Und schon funktioniert das Neu-Kompilieren nicht, weil irgendwas beim> Assembler nicht mehr stimmt. Dann debuggt man mühselig und ärgert sich> über den Blödsinn von damals.
Warum sollte denn ausgerechnet beim Assembler was nicht mehr stimmen?
Der bleibt doch genau gleich. Was sich ändern kann, ist das, was der
Compiler aus dem C-Code macht. Deshalb ist die Gefahr bei dem doch
eigentlich viel größer, daß er sich anders verhält.
Rolf M. schrieb:> Warum sollte denn ausgerechnet beim Assembler was nicht mehr stimmen?
So ziemlich alles, wenn man von einem AVR auf einen STM32 umsteigt. In C
kann man wenigstens den hardware-unrelevanten Teil annähernd 1:1
übernehmen, wenn man portabel genug programmiert hat.
Philipp schrieb:> Dass R3 nicht anderweitig gebraucht wird, habe ich vorher überprüft,> habe dazu den ASM Code nach R3 abgesucht, ist nirgendwo vorgekommen.
Alle Module sollten mit -ffixed-3 übersetzt sein. libgcc verwendet R3
nicht; einzige Ausnahme ist evtl. mit -mcall-prologues (__prologue_saves
und __epilogue_restores).
Philipp schrieb:> Selbst wenn man für den Vergleich den Wert von Zahnzaehler in Register> R20 bräuchte, wäre doch INC R3 gefolgt von MOV R20,R3 schneller.>> Erwarte ich vielleicht einfach zu viel von einem Compiler?
Ohne testfall ist das lesen aus dem Kaffeesatz. Evtl. eine Änderung
analog zu
https://gcc.gnu.org/r192198
Philipp schrieb:
noch mit einer Konstante, könnte das hier ausschlaggebend sein? Hier zur
> Übersicht nochmal die ganze Stelle:>
1
>Zahnzaehler++;
2
>LDIR20,0x01Loadimmediate
3
>ADDR20,R3Addwithoutcarry
4
>MOVR3,R20Copyregister
5
>if(Zahnzaehler==15){
6
>CPIR20,0x0FComparewithimmediate
7
>BRNEPC+0x05Branchifnotequal
8
>
>> Selbst wenn man für den Vergleich den Wert von Zahnzaehler in Register> R20 bräuchte, wäre doch INC R3 gefolgt von MOV R20,R3 schneller.
und wie gehts dann weiter?
Ev. kann der Compiler die 1 in R20 ja an späterer Stelle noch gut
gebrauchen.
Frank M. schrieb:> Rolf M. schrieb:>> Warum sollte denn ausgerechnet beim Assembler was nicht mehr stimmen?>> So ziemlich alles, wenn man von einem AVR auf einen STM32 umsteigt.
Ja, wenn ich auf eine komplett andere Architektur umsteige, natürlich.
Davon war aber nicht die Rede, sondern von:
PittyJ schrieb:> eine leicht geänderte Platine, oder das Prozessor-Nachfolgemodell, oder nur> ein neuer GCC.
Ich sage ja nicht, dass man C vermeiden soll, sondern nur, dass man,
wenn man ein zyklengenaues Timing machen will, durchaus Assembler nutzen
kann. Ich finde es jedenfalls albern, dann gleich einen um
Größenordnungen schnelleren Prozessor einzusetzen, nur damit man das
Timing auch anders hinbekommt.
In der avr gcc abi ist doch klar definiert welche Register vom Compiler
wofür verwendet werden und welche frei sind oder nicht?
Aber potentiell werden glaube ich alle verwendet, also sollte man da mit
gewisser Vorsicht herangehen. Lieber reines C
Mir ist klar, dass dass sich durch das Verkürzen des Codes um ein paar
Takte der Winkelfehler nicht Wesentlich ändern wird. Wahrscheinlich wird
selbst ein halbes Grad nichts ausmachen.
Habe aber den Ehrgeiz, auszutüfteln wie man durch "geschickte"
Programmstruktur den Code so schnell wir möglich zu machen. Habe wie
gesagt noch nichts derartiges Programmiert, deswegen habe ich -wie wohl
viele hier- noch nie drauf geachtet, wie der Assemblercode im Detail
aussieht.
Da aber überall behauptet wird, dass der Compiler ja sooo intelligent
wäre, hätte ich halt erwartet, dass er nicht dermaßen Takte verschenkt,
dass ich es sogar auf einen Blick im ASM Code sehe.
Dazu ist mir gleich das Nächste aufgefallen: Am Anfang einer ISR werden
teilweise Register auf den Stack gesichert, die in der ISR garnicht
geändert werden. 12 push Befehle + 12 pop Befehle, davon je 10 unnötig,
also 20! Takte verschenkt. Da wird mir wohl nichts anderes übrig
bleiben, als selbst in ASM Hand anzulegen.
Philipp schrieb:> 12 push Befehle + 12 pop Befehle, davon je 10 unnötig,> also 20! Takte verschenkt. Da wird mir wohl nichts anderes übrig> bleiben, als selbst in ASM Hand anzulegen.
Und um dann, wie oben schon bemerkt, bei jeder Programmänderung, jeder
neuen Compilerversion etc. zu überprüfen, ob sich die Registernutzung
nicht doch geändert hat. Welche Register werden benutzt, wenn mal ein
long oder ein long long im Code vorkommt?
MfG Klaus
Philipp schrieb:> Da aber überall behauptet wird, dass der Compiler ja sooo intelligent> wäre, hätte ich halt erwartet, dass er nicht dermaßen Takte verschenkt,> dass ich es sogar auf einen Blick im ASM Code sehe.
Guckst du dabei auch über den Tellerrand der jeweils aktuell von die
betrachteten Zeile C-Ursprung hinaus? Genau das tut der Compiler oft
genug, was zu unsinnig erscheinendem Assembler führen kann, weil während
der einen Berechnung schon die nächste vorbereitet wird, z.B. eben dass
Registerinhalte nebenbei abfallen. Oder auch nur Flags. Und so wird bei
der einen Anweisung 1 Takt investiert, um bei den nächsten drei jeweils
2 zu sparen. Sieh dir mal den Code daraufhin an.
Klar ist das nicht 100% perfekt, aber im Schnitt auf den gesamten Code
gesehen meistens besser als was man so händisch hinzutunen versucht.
Philipp schrieb:> Dazu ist mir gleich das Nächste aufgefallen: Am Anfang einer ISR werden> teilweise Register auf den Stack gesichert, die in der ISR garnicht> geändert werden.
Sowas macht er eigentlich nur dann, wenn von der ISR aus Funktionen
aufgerufen werden, die nicht inline aufgelöst werden. Da er dann nicht
weiß, welche Register innerhalb der Funktion benutzt werden, muss er
eben alle sichern, die die Funktion möglicherweise nutzt.
Kleine unproblematische Registeroptierung: Einige AVRs haben ein paar
freie Bytes im I/O-Bereich, GPIOR0..2. Die lassen sich kürzer und
schneller ansprechen als normaler Speicher und eignen sich besonders gut
für globale Flags/Einzelbits, weil sie von den Einzelbitbefehlen
angesprochen werden können.
Rolf M. schrieb:> Philipp schrieb:>> Dazu ist mir gleich das Nächste aufgefallen: Am Anfang einer ISR werden>> teilweise Register auf den Stack gesichert, die in der ISR garnicht>> geändert werden.>> Sowas macht er eigentlich nur dann, wenn von der ISR aus Funktionen> aufgerufen werden, die nicht inline aufgelöst werden. Da er dann nicht> weiß, welche Register innerhalb der Funktion benutzt werden, muss er> eben alle sichern, die die Funktion möglicherweise nutzt.
Da zeigt uns Philip dann mal den Sourcecode und den Assemblercode, in
dem das von ihm beobachtete auftritt. Dazu bitte auch die benutzten
Optionen.
Philipp schrieb:> Das würde nur 2 Takte brauchen und somit 1/3 der Rechenzeit einsparen!
Da es anscheinend noch niemand bemängelt hat: sbi und cbi brauchen auf
"normalen" avrs 2 Takte. Folglich wäre diese Lösung langsamer, würde
jedoch Speicher sparen. Nur auf den reduced core avrs (attiny4/5/9/10)
braucht der Befehl 1 Takt.