Habe mit dem neuen AVR-Studio 6 ein kleines Programm geschrieben und mit
-O0 (Null) compiliert - Zielprozessor ist der ATMEGA2560. Bei der
Sichtung des lss-Files fielen mir ein paar Ungereimtheiten im erzeugten
Code auf, die ich mir nicht erklären kann..
Ausgangsprogramm:
1
intmain(void)
2
{
3
unsignedchara=3,b=4,c;
4
while(1){
5
a++,b++;
6
c=a+b;
7
}
8
}
Assemblercode "lss-file" (nur von der main-Routine):
1
0000012a <main>:
2
12a: cf 93 push r28
3
12c: df 93 push r29
4
12e: 00 d0 rcall .+0 ; 0x130 <main+0x6>
5
130: cd b7 in r28, 0x3d ; 61
6
132: de b7 in r29, 0x3e ; 62
7
134: 83 e0 ldi r24, 0x03 ; 3
8
136: 89 83 std Y+1, r24 ; 0x01
9
138: 84 e0 ldi r24, 0x04 ; 4
10
13a: 8a 83 std Y+2, r24 ; 0x02
11
13c: 89 81 ldd r24, Y+1 ; 0x01
12
13e: 8f 5f subi r24, 0xFF ; 255
13
140: 89 83 std Y+1, r24 ; 0x01
14
142: 8a 81 ldd r24, Y+2 ; 0x02
15
144: 8f 5f subi r24, 0xFF ; 255
16
146: 8a 83 std Y+2, r24 ; 0x02
17
148: 99 81 ldd r25, Y+1 ; 0x01
18
14a: 8a 81 ldd r24, Y+2 ; 0x02
19
14c: 89 0f add r24, r25
20
14e: 8b 83 std Y+3, r24 ; 0x03
21
150: f5 cf rjmp .-22 ; 0x13c <main+0x12>
Könnt ihr mir vielleicht sagen, warum
1. in 12e ein "rcall .+0" steht? das macht ja ansich keinen Sinn, oder?
Es legt lediglich die aktuelle PC-Adresse+1 auf den Stack (=0x130) und
erhöht diesen, aber warum?
Dazu: verändere ich den Datentyp von "unsigned char" auf "unsigned
short" so werden 2 von diesen rcall-s eingefügt...
2. die Anweisungen "a++" und "b++" werden durch eine Subtraktion mit 255
realisiert (Zeilen 13e und 144) - warum? ist ein increment nicht
schneller/kürzer?
PS: ich habe nur wenig AVR-Erfahrungen, komme von 68000er und PIC und
daher ist mir vieles dabei noch unklar...
Rene F. schrieb:Rene F. schrieb:> 2. die Anweisungen "a++" und "b++" werden durch eine Subtraktion mit 255> realisiert (Zeilen 13e und 144) - warum? ist ein increment nicht> schneller/kürzer?
a) dauern eigentlich alle Befehle auf dem AVR einen Zyklus, und b) hat
der gar kein add immediate-Befehl ;) Daher der "Trick" mit der
Subtraktion von -1.
Über die "Kürze" von Code zu sinnieren, der ohne Optimierung kompiliert
wurde, ist allerdings sowieso ziemlich zweckfrei.
Oliver
Oliver schrieb:> Rene F. schrieb:>> 2. die Anweisungen "a++" und "b++" werden durch eine Subtraktion mit 255>> realisiert (Zeilen 13e und 144) - warum? ist ein increment nicht>> schneller/kürzer?>> a) dauern eigentlich alle Befehle auf dem AVR einen Zyklus, und b) hat> der gar kein add immediate-Befehl ;) Daher der "Trick" mit der> Subtraktion von -1.
aber einen INC-Befehl hat er schon ... das hatte mich verwundert, dass
nicht das Naheliegendste benutzt wird..
SUB hat ja immernoch den Nebeneffekt des gesetzen Carry-Flags und das
wird hierbei garnicht "korrigiert" - selbst bei automatisch generiertem
Code ohne Optimierung sollte sowas doch nicht erzeugt werden, wenn es
dadurch zu Problemen mit anderen Teilen kommen kann.
Rene F. schrieb:> aber einen INC-Befehl hat er schon ... das hatte mich verwundert, dass> nicht das Naheliegendste benutzt wird..> SUB hat ja immernoch den Nebeneffekt des gesetzen Carry-Flags und das> wird hierbei garnicht "korrigiert" - selbst bei automatisch generiertem> Code ohne Optimierung sollte sowas doch nicht erzeugt werden, wenn es> dadurch zu Problemen mit anderen Teilen kommen kann.
Wieso sollte das Carry-Flag "korrigiert" werden? Das kann überall im
Programm verändert werden...
Code, der das Carry-Flag auswertet, muss es natürlich zuvor richtig
setzen.
Und in Interrupts wird das SREG, und damit das Carry-Flag, gesichert und
dann wiederhergestellt.
Michael schrieb:> Rene F. schrieb:>> SUB hat ja immernoch den Nebeneffekt des gesetzen Carry-Flags und das>> wird hierbei garnicht "korrigiert" - selbst bei automatisch generiertem>> Code ohne Optimierung sollte sowas doch nicht erzeugt werden, wenn es>> dadurch zu Problemen mit anderen Teilen kommen kann.>> Wieso sollte das Carry-Flag "korrigiert" werden? Das kann überall im> Programm verändert werden...
stimmt schon, aber ich erwarte ja als Programmierer nicht, dass das
Carry-Flag auf 1 geht nur weil ich eine Zahl inkrementiere, oder?
Das macht debuggen dann schwieriger, wenn sich solch ein Bit verändert,
ohne dass die arithmetische Operation (a++) das erfordern würde.
nur in dieser Hinsicht ist das seltsam.
Rene F. schrieb:> stimmt schon, aber ich erwarte ja als Programmierer nicht, dass das> Carry-Flag auf 1 geht nur weil ich eine Zahl inkrementiere, oder?
Was du als Programmierer erwarten kannst, ist ja eigentlich nur, daß das
Assembler-Programm das tut, was dein C-Programm aus Sicht des
C-Standards tun sollte.
Daher ist es völlig nebensächlich, was der Compiler genau aus dem C-Code
macht, solange das Programm das tut, was du in den C-Code schreibst.
Und ob unsigned char a++ bei einem Überlauf das Carry-Flag setzt, oder
auch nicht, ist dem C-Standard herzlich egal. Der kennt gar kein
Carry-flag.
Oliver
Rene F. schrieb:> und mit> -O0 (Null) compiliert
...und dann auch noch erwartet, dass er trotzdem optimal ist.
Naja. Du hast noch einen Versuch, würde ich mal sagen. ;-)
Jörg Wunsch schrieb:> Rene F. schrieb:>> und mit>> -O0 (Null) compiliert>> ...und dann auch noch erwartet, dass er trotzdem optimal ist.
ihr habt ja Recht - dass es optimal sein würde, ist dann wirklich nicht
zu erwarten.
Aber ich würd gern den Compiler in dieser Hinsicht verstehen, warum
manches so oder so übersetzt wird - den "rcall" kann ich mir einfach
nicht erklären, den Sub vielleicht schon, wenn es denn eine der ersten
Übersetzungen ist, die im Compiler verdrahtet sind.
Hintergrund des ganzen ist, dass ich gern ein paar Studenten erklären
möchte, wie aus ihren C Programmen Maschinen-Befehle für den Prozessor
erzeugt werden ohne dass sie selbst Assembler programmieren müssen - und
da ist leider die Übersetzung ungeeignet. Bei einer Optimierung werden
ja viele Schritte zwischendrin schon ersetzt und hier im Beispiel wird
die main-funktion gleich ganz rausoptimiert...
Rene F. schrieb:> Aber ich würd gern den Compiler in dieser Hinsicht verstehen, warum> manches so oder so übersetzt wird - den "rcall" kann ich mir einfach> nicht erklären,
Wurde dir ja nun schon erklärt.
> den Sub vielleicht schon, wenn es denn eine der ersten> Übersetzungen ist, die im Compiler verdrahtet sind.
Der AVR hat keinen Befehl "ADDI", aber ein "SUBI". "ADDI" hat man
rausgeschmissen (zu Gunsten anderer Befehle), nachdem die IAR-
Entwickler im Entwurf des Ur-AVR angemerkt haben, dass dieser
Befehl überflüssig ist, da er stets durch ein SUBI ersetzbar ist.
(Kann man in einem uralten Dokument bei Atmel nachlesen.)
Damit wird eine Addition also erstmal generisch als Subtraktion
umgesetzt. Die Erkennung, dass die Addition ja nur mit der Zahl
1 erfolgt war und folglich ein INC genügt, ist dann Aufgabe des
Optimierers.
> Hintergrund des ganzen ist, dass ich gern ein paar Studenten erklären> möchte, wie aus ihren C Programmen Maschinen-Befehle für den Prozessor> erzeugt werden ohne dass sie selbst Assembler programmieren müssen - und> da ist leider die Übersetzung ungeeignet.
Dann solltest du etwas tiefer in den GCC einsteigen und einiges
des Zwischencodes mit heranziehen. Sieh dir mal das Ergebnis von
-fdump-rtl-all an.
> Bei einer Optimierung werden> ja viele Schritte zwischendrin schon ersetzt und hier im Beispiel wird> die main-funktion gleich ganz rausoptimiert...
Man kann an diversen Stellen die Optimierung verhindern mit paar
Tricks (attribute "used", volatile etc.).
Du solltest auf jeden Fall ein Beispiel finden, an dem man einerseits
die formal langweilige Codegenerierung ohne Optimierung und
andererseits die tiefgreifenden Änderungen, die die Optimierung dann
mit sich bringt, demonstrieren kann. Das gibt einem dann ein
Gefühl, dass ein Compiler nicht nur eine stur blöde Maschine ist
(obwohl er es trotzdem ist ;-), sondern dass darin viele clevere Leute
ihr Wissen eingebracht haben und im Ergebnis ein Maschinencode
entsteht, den ein Programmieranfänger mit reinem Assembler oft nicht
so kompakt hinbekommen hätte.
Nimm bitte die neueste GCC-Version dafür, Johann hat da noch einiges
an Mikro-Optimierungen im AVR-Bereich nachgezogen in letzter Zeit.
Rene F. schrieb:> int main(void)> {> unsigned char a=3, b=4, c;> while (1) {> a++, b++;> c=a+b;> }> }
Und schreib' nicht sowas sinnloses. Das macht nach aussen hin gar nichts
und wird deshalb natürlich wegoptimiert. Und Strom verbraucht der
Controller auch so.
Deklarierst du die Variablen aber global und volatile macht der Code
zwar immer noch keinen Sinn, aber es wird nicht komplett wegoptimiert.
Rene F. schrieb:> a) dauern eigentlich alle Befehle auf dem AVR einen Zyklus
Nein. Beispile: CPSE, RJMP, CALL, RET, RETI, MUL, LDS, ADIW ...
> b) hat der gar kein add immediate-Befehl ;)
Doch, allerdings nur für die W-Register, da geht dann ADIW mit
konstanten
0...63. 1...64 wär sinniger, ist aber nun mal so.
> Oliver schrieb:>> Rene F. schrieb:>>> 2. die Anweisungen "a++" und "b++" werden durch eine Subtraktion mit 255>>> realisiert (Zeilen 13e und 144) - warum? ist ein increment nicht>>> schneller/kürzer?>>>> b) hat der gar kein add immediate-Befehl ;)>> Daher der "Trick" mit der Subtraktion von -1.>> aber einen INC-Befehl hat er schon ... das hatte mich verwundert, dass> nicht das Naheliegendste benutzt wird.
Das naheliegendste is SUBI/SBIC, denn damit können alle Werte addiert
und subtrahiert und addiert werden, und die Flags werden auch richtig
gesetzt.
INC/DEC werden auf den unteren Registern verwendet.
Jörg Wunsch schrieb:> Dann solltest du etwas tiefer in den GCC einsteigen und einiges> des Zwischencodes mit heranziehen. Sieh dir mal das Ergebnis von> -fdump-rtl-all an.
Und natürlich -fdump-tree-all nicht vergessen und -dp -fverbose-asm
-save-temps.
Im erzeigten Assembler-Code ist das Resultat besser zu erfassen als in
einem Disassembly.
Hier mal ein kleines Modul, wo nicht alles wegoptimiert werden kann:
Wie man sieht, stehen da sogar Kommentare wozu das "rcall ." gut ist.
>> Bei einer Optimierung werden>> ja viele Schritte zwischendrin schon ersetzt und hier im Beispiel wird>> die main-funktion gleich ganz rausoptimiert...>> Man kann an diversen Stellen die Optimierung verhindern mit paar> Tricks (attribute "used", volatile etc.).>> Du solltest auf jeden Fall ein Beispiel finden, an dem man einerseits> die formal langweilige Codegenerierung ohne Optimierung und> andererseits die tiefgreifenden Änderungen, die die Optimierung dann> mit sich bringt, demonstrieren kann.
ACK.
C hat noch viele andere Möglichkeiten als den Holzhammer [tm] volatile
wenn man eine Funktion haben will, die Seiteneffekte hat, damit darin
nicht alles bis zur Trivialität weggestrichen wird.
> Nimm bitte die neueste GCC-Version dafür, Johann hat da noch einiges> an Mikro-Optimierungen im AVR-Bereich nachgezogen in letzter Zeit.
Sowie ich das verstehe, geht es darum, dem Compiler bei der Arbeit
zuzuschauen und eine Vorstellung davon zu bekommen, was er so treibt.
Der ausgegebene Assembler-Code ist nicht immer gut verständlich, das war
in den älteren Versionen teilweise besser. 4.6 gibt für f3 von oben
etwa folgendes aus:
Rene F. schrieb:> aber einen INC-Befehl hat er schon ... das hatte mich verwundert, dass> nicht das Naheliegendste benutzt wird..
Ob Naheliegend oder nicht ist etwas, das im Auge des Betrachters liegt.
Hier, in diesem konkreten Beispiel ist es völlig Wurscht ob SUBI oder
INC. Beide Befehle erfüllen hier den gleichen Zweck.
> SUB hat ja immernoch den Nebeneffekt des gesetzen Carry-Flags und das> wird hierbei garnicht "korrigiert"
Und?
Im konkreten Beispiel stört das ja nicht.
@Jörg und @Johann... vielen dank für die Hinweise! ich gehe schon auf
mehr als nur den Assembler-code ein und werde mal sehen, was ich aus dem
gcc noch an Informationen zum Verständnis bekommen kann.
@Thomas... dass der Code unsinnig ist, weiss ich auch - es sollten ja
eben nur ein paar einfache Befehle sein und man daran die Umsetzung in
Assembler sehen und ich dachte eben, dass arithmetische Operationen fast
1:1 übersetzt werden.
@KarlHeinz... stimmt schon, dass es nicht stört, wenn das Carry gesetzt
wird oder nicht. mein Verständnis vom ersten Compilerschritt war, dass
jede Anweisung zunächst in ein Assemblerkonstrukt transformiert wird
(ja, zwischendrin mit Syntaxbaum,Abhängigkeiten, etc.). Und das würde ja
heissen, dass erstmal jeder Inkrement zu einem SUBI wird - egal was
rundrum steht.
Nun hat der atmel ja verschiedene SUB-Befehle (mit und ohne Carry) und
deshalb ist es in dem Beispiel auch egal, was damit passiert - bei den
PICs gibt es eben nur einen SUB-Befehl und der ist immer mit Carry,
weshalb es mich so verwundert hatte...
wie gesagt: meine Atmel-Erfahrungen stehen noch am Anfang und ich bin
für eure Hilfe wirklich dankbar.
Rene F. schrieb:> mein Verständnis vom ersten Compilerschritt war, dass> jede Anweisung zunächst in ein Assemblerkonstrukt transformiert wird
Nein. Im Gegenteil, dieser Schritt erfolgt sogar erst recht spät.
Intern wird sehr lange noch mit einer abstrakten Beschreibung
gearbeitet. Auf diese Weise kann man einen Großteil der Optimierungen
bereits unabhängig von der Zielmaschine vornehmen.
(Johann möge mich korrigieren, wenn ich hier schief liege, er steckt
da tiefer drin.)
Rene F. schrieb:> @Jörg und @Johann... vielen dank für die Hinweise! ich gehe schon auf> mehr als nur den Assembler-code ein und werde mal sehen, was ich aus dem> gcc noch an Informationen zum Verständnis bekommen kann.
Das Problem ist meines Erachtens, dass reale Compiler zu kompliziert
sind und zu viele Nebenbedingungen beachten müssen, um damit jemanden
der da nicht so involviert ist, die grundsätzliche Arbeitsweise eines
Compilers nahe zu bringen.
Mir hat es am Anfang (in der Vorlesung Compilerbau) sehr geholfen, dass
wir erst mal einen Compiler für eine fiktive Maschine gebaut haben.
Konkret war das eine Stack-Maschine so dass der ganze Themenkreis
"Registerallokierung" erst mal weggefallen ist. Mit so einer
Stackmaschine ist es dann auch nicht so schwer, zb den Themenkreis
"Expression Parsing" nachzuvollziehen und in eigenen Programmen mal
damit zu experimentieren.
> @Thomas... dass der Code unsinnig ist, weiss ich auch - es sollten ja> eben nur ein paar einfache Befehle sein und man daran die Umsetzung in> Assembler sehen und ich dachte eben, dass arithmetische Operationen fast> 1:1 übersetzt werden.
Ist ja grundsätzlich nicht von der Hand zu weisen. Nur solltest du dich
nicht auf C-typische Sonderfälle wie die Inkrement Operatoren
versteifen, bzw. die die pragmatische Sichtweise aneignen, dass
i++;
auch nur eine andere Schreibweise für
i = i + 1;
ist, die es dem Compiler ermöglichen kann, anderen Code zu generieren
und zwar ohne, dass man im Compiler riesigen Aufwand treiben muss. Die
Inkrement Operatoren sind also mehr 'Hinweise' an den Compiler, damit
der ohne große Datenflussanalyse zum gleichen Ergebnis kommt.
Welche CPU Instruktionen dann tatsächlich benutzt werden, ist damit
überhaupt nicht ausgesagt. Ein i++ kann in einem INC münden, muss es
aber nicht. Wenn die Kosten für einen INC und einen ADI (oder so wie
hier SUBI) identisch sind, kann es für den Compilerbauer einfacher sein,
die Fälle
i++;
i = i + 1;
i = i + 5;
über einen gemeinsamen Kamm zu scheren. Es ändert sich nur die
Additionskonstante. In 2 Fällen ist sie 1 und in einem Fall ist sie 5.
Aber vom Prinzip her ist es immer die gleiche Operation.
In Baumsicht ist das
Assign
/ \
/ \
Destination Add
/ \
/ \
Source Constant
Sind 'Source' und 'Destination' gleich, dann kann dieser Ausdruck unter
Umständen zu einem 'Inkrement' vereinfacht werden. Kann - muss aber
nicht. Denn dieser Teilbaum ist ja wieder in einen größeren Kontext
eingebunden.
> @KarlHeinz... stimmt schon, dass es nicht stört, wenn das Carry gesetzt> wird oder nicht. mein Verständnis vom ersten Compilerschritt war, dass> jede Anweisung zunächst in ein Assemblerkonstrukt transformiert wird> (ja, zwischendrin mit Syntaxbaum,Abhängigkeiten, etc.).
Du unterschätzt die Zwischenschritte. Die haben viel mehr Bedeutung als
dann hinten nach die eigentliche Assemblergenerierung. Die ist mehr als
Postprozess zu sehen, die von einer abstrakten Zwischenschicht auf die
tatsächlichen Befehle abbildet. Die eigentliche Compilerarbeit findet
auf diesen Zwischenschichten statt! Die Assemblergenerierung ist mehr
als Postprozess zu sehen. D.h. dieser Teil weiß unter Umständen gar
nicht mehr, dass da ursprünglich mal ein ++ stand.
> Nun hat der atmel ja verschiedene SUB-Befehle (mit und ohne Carry) und> deshalb ist es in dem Beispiel auch egal, was damit passiert
Nicht ganz.
Ohne jetzt für alle Prozessoren verallgemeinern zu wollen:
Arithmetische Operationen berücksichtigen das Carry Flag oder sie
berücksichtigen es nicht - in der Arbeitsweise der Operation. Aber die
Flags werden üblicherweise von der Operation immer beeinflusst. D.h.
egal ob ein SUB oder ein SBC benutzt wird, das Carry Flag hat am Ende
der Operation immer eine gewissen Bedeutung und ist von der Operation
beeinflusst worden (genauso wie das Zero Flag, das Half Carry Flag und
was da sonst noch so kreucht und fleucht)
Rene F. schrieb:> 2. die Anweisungen "a++" und "b++" werden durch eine Subtraktion mit 255> realisiert (Zeilen 13e und 144) - warum? ist ein increment nicht> schneller/kürzer?
Und was ist, wenn a und b kein char sondern uint16_t sind? dann geht
auch wieder inc nich...
MfG Dennis