Hallo,
ohne wirklich viel Ahnung von Programmierung in Assembler und der
Funktionsweise des GCC sowie seiner Hilfsprogramme zu haben, habe ich
ein testweises Programm geschrieben und mir mal mit verschiedenen
Arbeitsweisen im Programm sowie verschiedenen Optionen übersetzen
lassen.
Allgemein ist ja bekannt, dass der GCC (Optimierungsstufe -Os) hieraus
1
uint8_tv;
2
3
// ...
4
5
if(v&1){
6
// tu was
7
}
8
9
// ...
gerne einen 16-bit Vergleich macht:
1
; ...
2
3
movw r20,r24
4
andi r20,lo8(1)
5
andi r21,hi8(1)
6
cp r20,__zero_reg__
7
cpc r21,__zero_reg__
8
breq .L2
9
10
; tu was
11
12
.L2:
13
14
; ...
Lässt man den GCC das hier übersetzen, sieht es schon besser aus:
1
uint8_ttemp;
2
uint8_tv;
3
4
// ...
5
6
temp=1;
7
if(v&temp){
8
// tu was
9
}
10
11
// ...
1
mov r20, r24
2
andi r20,lo8(1)
3
tst r20
4
breq .L2
5
6
; tu was
7
8
.L2:
9
10
; ...
Das selbe Ergebnis bekomme ich mit der GCC-Option -mint8. Mit einem Cast
auf der 1 ...
1
if(v&((uint8_t)1)){//...
... ist lustigerweise alles mit 16bit verarbeitet!?
Das dies Verhalten nicht erwünscht ist steht außer Frage. Ich habe
mehrfach gelesen, dass man -mint8 vermeiden soll, da es nicht ausgereift
sei und einige Bibliotheken dann nicht mehr kompatibel sind.
Was kann man dann am besten dagegen tuen? Wie händelt ihr das Problem?
Ein ständiges kopieren in eine temporäre Variable finde ich nervig und
ein (funktionierender) Cast ist auf Dauer auch nicht mehr
schön/leserlich.
LG :)
J. W. schrieb:> Was kann man dann am besten dagegen tuen? Wie händelt ihr das Problem?
Ignorieren. Wenn man sich über jeden Kleinkram an verpasster Optimierung
aufregt kriegt man nur Magengeschwüre.
Nur wenns an bestimmten Stellen wirklich auf jeden einzelnen Takt
ankommt mache ich mir über solche Kleinigkeiten wirklich Gedanken.
A. K. schrieb:> Ignorieren. Wenn man sich über jeden Kleinkram an verpasster Optimierung> aufregt kriegt man nur Magengeschwüre.
Ja. Ich kriege auch so schon "Magengeschwüre", nur weil ich mir den
Umstand mal genauer angeschaut habe.
Ignorieren ist natürlich eine Möglichkeit. Aber in manchen Fällen kann
man den Controller nicht weiter hoch skalieren um mehr Speicher zu haben
und bekommt dann wegen sowas gewünschte Funktionen nicht mehr in den
Flash. Und da fängt es an, mich aufzuregen.
Das man das Ignorieren kann (und das teilweise auch nicht interessiert)
ist mir durchaus klar. Aber das hilft mir in diesem Fall leider gar
nicht.
Daher wäre ich sehr dankbar, wenn ihr mir in euren Antworten noch andere
Möglichkeiten darstellen könntet, die das Problem wirklich beheben. ;)
LG :)
Ich steh grad auch etwas dumm da. :p Ich habe gerade meine Testdatei
etwas ausgeschlachtet (waren noch diverse andere Sachen/Vergleiche
drin). Der Include von stdint.h fehlte, dafür war avr/io.h includiert.
avr-gcc Aufruf (Version 4.3.3):
> avr-gcc -save-temps -Os -mmcu=attiny2313 test.c
Leider ist das Phänomen oben seit dem Ausschlachten weg. Egal ob mit
oder ohne stdint inkludiert.
Ich versuche mal das wieder herzustellen und geb euch dann mal das
komplette Test- und Assemblerfile.
LG :)
A. K. schrieb:> Habs grad mit gcc 4.5.3 ausprobiert und es kommt raus:> sbrs r24,0> rjmp .L2
Ja, weil du es für ein Device mit mehr als 8 KiB Flash übersetzt hast.
So,
das Phänomen hat noch ganz andere Ausmaße :p
Ansich geht es nicht darum, ob die logische Operation (AND + Vergleich)
mit 16-bit ausgeführt wird, sondern dass v selbst als 16-bit behandelt
wird.
Zumindest solange es selbst nicht verändert oder initialisiert wird.
Ich habe zwei Versionen der C- und AVRASM-Dateien angehängt. In der
einen, die "vernünftigen Code" erzeugt, wird v am Ende der Hauptschleife
inkrementiert (oder besser gesagt, es wir -1 abgezogen ;)).
In der anderen fehlt dieser Inkrement.
Ich habe v bewusst nicht initialisiert, sonst wird alles weg optimiert.
GCC Version 4.3.3, GCC Aufruf mit
> avr-gcc -mmcu=attiny2313 -save-temps -Os test_buggy(16).c
Das erklär mir jetzt mal einer :p
Yalu X. schrieb:> ergibt bei mir mit den GCC-Versionen 4.2 bis 4.6 folgendes Ergebnis:> test:> lds r24,v> sbrc r24,0> rcall tuwas> ret>> Besser geht's nicht.
Resultiert daraus, dass -mint8 überflüsig ist, da das Problem in neueren
GCC-Versionen nicht mehr besteht?
LG :)
PS: Sehe grad, das in test_buggy.c das v++ auch drin steht. Bitte
wegdenken ;)
Johann L. schrieb:> Ja, weil du es für ein Device mit mehr als 8 KiB Flash übersetzt hast.
Ich habs für garnix übersetzt, keine Ahnung was der Compiler dann nimmt.
Sieht aber bei -mmcu=attiny2313 auch nicht anders aus.
Also was ist jetzt falsch? Du verwendest nicht-initialisierte Variablen.
garbage in -- garbage out
Ansonsten sieht der Code korrekt aus und entspricht dem, wenn das v++
entfernt wird.
Johann L. schrieb:> Ja, weil du es für ein Device mit mehr als 8 KiB Flash übersetzt hast.
NB: Da du das sicherlich nicht grundlos schreibst: Wie entsteht so ein
Zusammenhang?
Johann L. schrieb:> v ist schleifeninvariant und der Compiler zieht die &-Operationen aus> der Schleife.
Ok, allerdings kriege ich keinen Unterschied bei der 8K Grenze.
Ausserdem zieht er nur eine der Operationen raus.
Johann L. schrieb:> Also was ist jetzt falsch? Du verwendest nicht-initialisierte Variablen.
Seit wann soll das verboten sein? Tatsache ist doch, dass v eine 8-bit
Variable ist. Wie kommt der GCC dann drauf (initialisierung hin oder
her), da irgendwas mit 16-bit rechnen zu wollen?
A. K. schrieb:> Die C Versionen sehen für mich ziemlich identisch aus.
Dann hättest du genauer lesen sollen. Tut mir Leid, dass ich da im
Dateistrudel nen Fehler rein bekommen habe:
J. W. schrieb:> PS: Sehe grad, das in test_buggy.c das v++ auch drin steht. Bitte> wegdenken ;)
LG :)
J. W. schrieb:> Seit wann soll das verboten sein?
Das Ergebnis von solchem Code ist undefiniert.
> Wie kommt der GCC dann drauf (initialisierung hin oder> her), da irgendwas mit 16-bit rechnen zu wollen?
GCC ist nicht für 8-Bit Architekturen optimiert. Die Compilerbastler wie
Johann tun einiges, um die dadurch entstehenden 16-Bit Artifakte
wegzuoptimieren. Aber es wird immer gewisse Reste geben, in denen man
erkennt, dass die Grundeinheit von GCC ein 16-Bit Register ist (sizeof
int).
Das ist eine hinnehmbare Erklärung. Trotzdem irritiert mich, dass das
bei einer 8-bit Variablen plötzlich 16-bit Berechnung auf einem 8-bit
Controller erzwungen wird.
A. K. schrieb:> Das Ergebnis von solchem Code ist undefiniert.
OK, das ist mir neu. Liegt das jetzt nur daran, dass er nicht
initialisiert ist oder ist hinreichend Bedingung dafür auch, dass der
Zustand von v nirgendwo gesetzt wird.
Kurz: Führt das hier auch zu einem undefinierten Ergebnis:
Der Compiler zieht hier ein Zwischenergebnis aus der Schleife raus und
da alle Rechenoperationen mit mindestens sizeof(int) gerechnet werden
ist diese Variable nun 16 Bits breit. Der Rest folgt daraus.
Diese Optimierung ist global, für alle Architekturen, und kümmert sich
nicht um die Frage, ob eine Teilwortoperation möglicherweise billiger
ist als eine Maschinenwortoperation. Für GCC hat das Maschinenwort auch
bei AVRs 16 Bits. Viele Stellen, wo der erzeugte Code dennoch rein
8-bittig rechnet, sind spezielle Optimierungen für AVRs. Das hat aber
Grenzen.
J. W. schrieb:> Kurz: Führt das hier auch zu einem undefinierten Ergebnis:
Nein. Schreibender Zugriff auf eine nicht intialisierte Variable ist
kein Problem, sehr wohl aber lesender Zugriff.
A. K. schrieb:> Johann L. schrieb:>>> Ja, weil du es für ein Device mit mehr als 8 KiB Flash übersetzt hast.>> Ich habs für garnix übersetzt, keine Ahnung was der Compiler dann nimmt.
Er nimmt dann avr2
Für alle Versionen, die ich angetestet habe, compiliert
1
unsignedcharv;
2
3
voidtest(void)
4
{
5
if(v&1)
6
tuwas();
7
}
mit
>> avr-gcc foo.c -Os -S
zu
1
test:
2
lds r24,v
3
sbrc r24,0
4
rcall tuwas
5
.L1:
6
ret
insbesondere auch 4.5.0 und 4.5.2, wobei in der 4.5.2 offenbar
WinAVR-Patches drinne sind (__do_clear_bss steht am Dateiende wegen
PR18145) und in der 4.5.0 nicht.
Einige Devices haben einen Silicon-Bug, so daß kein Skip über eine
32-Bit Instruktione gemacht werden darf.
Ausserdem nimmt avr-gcc < 4.7 als Kriterium für einen Skip die
Insn-Länge, die in 16-Bit Words angegeben ist. Ein Skip wird nur über
Length=1 gemacht, denn nur anhand von Length=2 kann nicht entschieden
werden, ob es sich um eine 32-Bit Instruktion handelt.
Diese Optimierung hat erst avr-gcc 4.7 (PR49939)
Beispiel:
Yep, das ergibt Sinn. Ich hatte das erst so verstanden, dass die
Optimierung des schleifeninvarianten Ausdrucks von der Flashgrösse
abhinge. Und das erschien mir etwas schräg.
A. K. schrieb:> Yep, das ergibt Sinn. Ich hatte das erst so verstanden, dass die> Optimierung des schleifeninvarianten Ausdrucks von der Flashgrösse> abhinge. Und das erschien mir etwas schräg.
Ist aber so: Ein CALL gibt's eben nicht bei kleinem Flash, da kann immer
ein RCLL stehen. Ausserdem ist nicht zu sehen, was hinter deiner
Sprung-Sequenz noch kommt.
Johann L. schrieb:> Ist aber so: Ein CALL gibt's eben nicht bei kleinem Flash, da kann immer> ein RCLL stehen. Ausserdem ist nicht zu sehen, was hinter deiner> Sprung-Sequenz noch kommt.
Jo, als Folge der Längenabschätzung geht das noch an. Allerdings kam bei
meinen Versuchen kein Unterschied in der Optimierung des Ausdrucks raus,
sondern bloss in der Frage, obs einen Skip oder einen Sprung drüber
gibt. Andererseits habe ich keinen kompletten Zoo aus GCCs parat,
sondern in solchen Fällen bloss ein Debian-Package, was zudem
tendentiell eher keine lebensnotwendigen Patches enthält.
Johann L. schrieb:> Die LIM (Loop Invariant Motion) erweist sich für AVR eher als> unvorteilhaft;
Ja, daran hatte ich in diesem Zusammenhang auch schon gedacht. Nicht die
erste und sicher nicht die letzte Optimierung, die bei AVRs tendentiell
in die Hose geht.
J. W. schrieb:> Das ist eine hinnehmbare Erklärung. Trotzdem irritiert mich, dass das> bei einer 8-bit Variablen plötzlich 16-bit Berechnung auf einem 8-bit> Controller erzwungen wird.
ISO C sieht das so vor. Dort wird vorgegeben, daß bei Berechnungen alle
Operanden, die kleiner als int sind, erstmal auf int hochkonvertiert
werden ("integer promotion"), bevor die Berechnung durchgeführt wird.
Damit wird auch dein & per Default erstmal mit 16 Bit berechnet. Der
Compiler darf das aber entsprechend optimieren, wenn er garantieren
kann, daß das Ergebnis dadurch nicht verändert wird.
Beim gcc ist jetz das Problem, daß er nie dafür ausgelegt war, irgendwas
mit weniger als int zu berechnen, weil das bei allen "normalen"
Zielplattformen keinen Vorteil bringt. Daher fehlen einfach die
entsprechenden Optimierungen. Und da AVR eher ein Nischen-Target ist,
geht die Entwicklung da nicht so flott voran.
Rolf Magnus schrieb:> Damit wird auch dein & per Default erstmal mit 16 Bit berechnet.
Sicher? Wenn ich das hier richtig verstehe (was nicht der Fall sein
muß...), dann werden die Operanden beim Bit-Operatore & nicht vorher
"promoted".
Oliver
Ganz sicher. Der Compiler muss sich so verhalten, als ob das so sei. Was
er real macht ist dann seine Sache, es muss aber das Gleiche rauskommen.
Führt beispielsweise dazu, dass
uint8_t i;
if (i+1 < 100) ...
meist nicht in 8-Bit Rechnung durchgeführt werden kann. Geht sonst
schief bei i=255.
Bei Bitoperationen & | ^ ist das zwar i.d.R. harmlos, aber GCC ist meist
nicht darauf eingerichtet, Teilwortoperationen als möglicherweise
effizienter als Wortoperationen zu betrachten. Bei den fast allen
Targets ist es nämlich andersrum, oder zumindest egal.
Oliver schrieb:> Rolf Magnus schrieb:>> Damit wird auch dein & per Default erstmal mit 16 Bit berechnet.>> Sicher?
Ja. Hab's nochmal nachgeschlagen. Beim bitweisen & (Kapitel 6.5.10)
steht dort unter Semantics: "The usual arithmetic conversions are
performed on the operands.", und zu diesen gehört nach Kapitel 6.3.1.8
auch die integer promotion.
Wie Rolf bereis mehrfach angemerkt hat, gelten die C-Regeln. Es ist ja
auch ein C-Programm bzw. -Modul. Ubersetzt man es z.B. mit avr-gcc 4.3.3
(zB WinAVR-20100110) mit
>> avr-gcc foo.c -Os -S -dP -fdump-tree-original -fdump-tree-optimized
Dann sieht das original-dump so aus:
1
;;Functionfoo
2
{
3
if(((int)a&4)!=0)
4
{
5
c=0;
6
}
7
}
und das optimized hat immer noch das "(int) a":
1
foo(a)
2
{
3
<bb2>:
4
if(((int)a&4)!=0)
5
goto<bb3>;
6
else
7
goto<bb4>;
8
9
<bb3>:
10
c=0;
11
12
<bb4>:
13
return;
14
}
Schaut man dann ins erzeugt Assembler ist da zu finden
1
;(set (pc)
2
; (if_then_else (eq (zero_extract:HI (reg:QI 24)
3
; (const_int 1)
4
; (const_int 2))
5
; (const_int 0))
6
; (label_ref:HI 17)
7
; (pc)))
8
sbrs r24,2 ; 9 *sbrx_branch [length = 2]
9
rjmp .L3
Im Kommentar steht die algebraische Darstellung der Instruktionssequenz
darunter, welcher bedeutet:
>> Extrahiere ein Bitfeld der Länge 1 beginnend ab Bit 2 aus dem>> 8-Bit Register R24, und mache einen zero-extend zu einem 16-Bit>> Wert. Vergleiche diesen Wert mit 0, und falls der Vergleich "wahr">> ist, springe zu Label #17; ansonsten mache keinen Sprung.
Die reale Maschine (das Programm auf dem AVR) macht nun genau das, was
die abstrakte Maschine (die durch die C-Spezifikation beschriebene)
vorschreibt.
In 4.7 sieht die Sequenz übrigens anders aus:
1
; (set (pc)
2
; (if_then_else (eq (zero_extract:QI (reg:QI 24)
3
; (const_int 1)
4
; (const_int 2))
5
; (const_int 0))
6
; (label_ref:HI 13)
7
; (pc)))
8
sbrc r24,2 ; 8 *sbrx_branchqi [length = 2]
Hier steht sinngemäß das gleiche, ausser
>> ... und mache einen zero-extend zu einem 8-Bit Wert ...
Es wurde also schon vor der Ausgabe erkannt, daß das High-Byte nicht für
diese Operation benötigt wird. Der Effekt ist aber immer noch der
gleiche: Das reale Programm verhält sich so, wie es die abstrakte
Maschine beschreibt.
Daß die Insn als SBRC und nicht als SBRS+RJMP ausgegeben wird ist
übrigens eine andere Optimierung, die mit Promotion nix zu tun hat.