Hallo,
ich verwende avr-gcc zur Kompilierung für einen Atmega.
Ich habe jetzt schon mehrfach beobachtet, dass es den Code aufbläht,
wenn man mit globalen Variablen rechnet, anstatt mit temporären, lokalen
Variablen, die man dann am Ende der globalen Variable zuweist.
Warum ist das so? Sollte der Compiler nicht eigentlich sowas automatisch
optimieren? Ich finde, es macht die Sache auch immer etwas
unübersichtlich.
Wenn die globale Variable volatile wäre, wäre es das ja einleuchtend.
Ist sie aber in dem Fall gar nicht mal.
Kurt Schluss schrieb:> Sollte der Compiler nicht eigentlich sowas automatisch> optimieren?
Kann er aber nur nur dann, wenn er genau weiss, dass zwischenzeitlich
niemand auf die Variable zugreift. Funktionsaufrufe und
Aliasing-Potential können ihn daran hindern.
Kurt Schluss schrieb:> Warum ist das so? Sollte der Compiler nicht eigentlich sowas automatisch> optimieren?
Der AVR-GCC erzeugt schon recht brauchbaren Code, macht aber einige
unnötige Umstände.
Ich nehme auch oft lokale Zwischenvariablen, um ihm auf die Sprünge zu
helfen.
Insbesondere bei Pointerzugriffen lohnt sich das sehr.
Wenn Du was besseres willst, nimm den IAR.
Peter D. schrieb:> macht aber einige unnötige Umstände.
Naja, das solltest du schon näher erläutern.
Die Antwort vor deiner trifft die Sache wohl deutlich genauer.
Peter D. schrieb:> Z.B. hier sind die 2 RJMP überflüssig:
Hat aber nichts mit einer globalen Variablen zu tun.
Die Ursachen, warum beim Zugriff auf eine globale Variable der Code
umständlicher wird im Vergleich zur lokalen, beschreibt die erste
Antwort recht gut. Wenn man's genauer haben will, müsste man den Code
im Detail sehen.
(Das soll nicht heißen, dass deine Bemerkung falsch wäre, aber die
müssten wir in einem separaten Thread diskutieren.)
Wer mal sehen will, auf welche skurrilen Lösungen Compiler kommen
können, der sollte sich mal das Resultat für amd64 ansehen:
andl $8, %edi
cmpb $1, %dil
sbbl %eax, %eax
andl $-78, %eax
addl $123, %eax
movb %al, PORTB(%rip)
Das lag mir natürlich auf der Zunge, denn darauf kommt ein
Asm-Programmierer nicht so leicht. Ich liess es aber lieber weg bevor
der Zirkus hier auch noch losgeht. Ist auch nicht wirklich optimal, auf
Basis von CMOV gehts besser.
Das Problem bei AVR (und auch Thumb, nicht aber ARM/Thumb2) liegt evtl.
darin, dass der Compiler die Initialisierung wegschiebt und auf einen
conditional select hinarbeitet, dann aber keinen hat. Also auf
r = (cond) ? in1 : in2;
statt
r = in1;
if (cond) r = in2;
Nimmt man statt "45" einen Parameter, dann ist der Spuk weg und auch AVR
machts richtig.
Ja, aber wie gesagt, in diesem Thread hat der TE ein ganz anderes
Problem. Vielleicht reicht er uns ja mal ein wenig (compilierbaren)
Beispielcode nach.
Jörg W. schrieb:> Hat aber nichts mit einer globalen Variablen zu tun.
Sollte auch nur ein Beispiel dafür sein, daß der AVR-GCC manchmal recht
eigensinnig ist.
Wenn man sich über Code ärgert, stellt man ihn um und hebt ihn selten
auf. Daher hab ich kein Beispiel griffbereit. Es ist mir aber
aufgefallen.
Mir ist auch mal das Gegenteil passiert, daß er 3 globale Variablen
unnötig in Register geladen hat, obwohl ein switch/case nur je eine
davon geändert hat. Dadurch gingen ihm aber Register verloren, so daß er
für lokale Variablen extra einen Stackframe anlegen mußte. Auch da habe
ich den fetten Code leider nicht aufgehoben.
Auch bei Arrayzugriffen auf Structs berechnet er gerne die Basisadresse
immer wieder neu. Erst mit einem Hilfspointer kann man ihn überreden,
direkt mit Displacement auf das Element zuzugreifen.
Im Scheduler Beispiel benutze ich das (-> Operator).
Ich würde mir aber wünschen, daß ein gut optimierender Compiler auch
beim . Operator einen gleich kompakten Code erzeugt, d.h. von sich aus
einen Basepointer anlegt.
Ich will mich hier aber nicht beklagen, für einen freien Compiler ist
die Codedichte ja ausreichend.
Vom IAR würde ich aber mehr erwarten.
Das Dumme ist, dass man alle solche Fälle jeweils einzeln aufdröseln
muss (deshalb brauchen wir hier auch separate Threads dafür). Johann
hat ja verdammt viel davon in den letzten Jahren schon abgeklappert und
bessere Lösungen eingebaut.
Mal sehen, ob sich der TE nun nochmal meldet.
Konkretes Beispiel.
Ich brauche zur Kontrolle im Programm eine ganz simple Checksumme über
den Flash. Die soll in einer globalen Variable stehen und wird einfach
beim Start generiert.
Jörg W. schrieb:> Kurt Schluss schrieb:>> Konkretes Beispiel.>> Bitte compilierbar.
... zum Beispiel wegen der konkreten Datentypen von Variablen. Die sind
immer wichtig, wenn es um Optimierungsprobleme geht.
> Konkretes Beispiel. [...]
Das liegt daran, dass zwischen den Zugriffen auf die globale Variable
andere Funktionen aufgerufen werden. Diese könnten den Inhalt der
globalen Variable ändern, deshalb muss der Compiler den Inhalt jedesmal
neu lesen. Bei lokalen Variablen weiss der Compiler, dass
Unterfunktionen die lokale Variable nicht ändern können und er kann die
Zugriffe schön optimieren.
asdfasd schrieb:> Das liegt daran, dass zwischen den Zugriffen auf die globale Variable> andere Funktionen aufgerufen werden.
Eigentlich nicht, die Dinger expandieren zu inline asm.
Deshalb ist es ja wichtig, dass man etwas Compilierbares hier hat,
damit man das auseinander klamüsern kann.
Peter D. schrieb:> Z.B. hier sind die 2 RJMP überflüssig:
Hauptproblem sind Kostenbeschreibung im avr-gcc. Dein Beispiel erzeugt
mit -mbranch-cost=2 kleineren Code. Allerdings erhalte ich mit dem
Default (0) durchaus kleineren Code; im letzten Projekt sind das rund 2%
(bei ca. 14k Binärcode).
Die Kosten zu überarbeiten wäre sehr zeitaufwändig und wird dadurch
erschwert, dass es keine brauchbaren Benchmarks gibt. Momentan haben
Konstanten Kosten 0, was noch Denis stammt:
http://gcc.gnu.org/viewcvs/gcc/trunk/gcc/config/avr/avr.c?revision=230016&view=markup#l9902A. K. schrieb:> Wer mal sehen will, auf welche skurrilen Lösungen Compiler kommen> können, der sollte sich mal das Resultat für amd64 ansehen:
Find ich naheliegend und nicht skurril :-) Ein Flag wird zu 0 resp. -1
gewandelt und dies als Maske, Summand, o.ä. verwendet. avr-gcc macht
das z.B. bei
1
#include<stdfix.h>
2
3
fractaddss(fracta,satfractb)
4
{
5
returna-b;
6
}
1
addss:
2
sub r24,r22
3
sbc r25,r23
4
brvc 0f
5
ldi r25,0x7f
6
cp r25,r23
7
sbc r24,r24
8
sbci r25,-1
9
0:
10
ret
A. K. schrieb:> Ist auch nicht wirklich optimal, auf Basis von CMOV gehts besser.
IIRC hatte ich damit mal rumgespielt, aber das wurde dann ein Fass ohne
Boden... Aber nett wär's schon, z.B für Rx unsigned:
1
Ry=Rx<const1?-1:const2;
1
cpi Rx, const1
2
sbc Ry, Ry
3
ori Ry, const2
===
1
Ry=Rx<const1?const2:0;
1
cpi Rx, const1
2
sbc Ry, Ry
3
andi Ry, const2
===
1
if(Rx<const1)
2
Ry++;
1
cpi Rx, const1
2
adc Ry, __zero_reg__
===
1
if(Rx>=const1)
2
Ry++;
1
cpi Rx, const1
2
sbci Ry, -1
Wobei anstatt const sinngemäß auch ein Register stehen kann.
Johann L. schrieb:> Find ich naheliegend und nicht skurril :-)
Du meinst, Asm-Experten wie Mobi verwenden routinemässig solchen Code?
Direkt selbsterkärend ist er jedenfalls nicht. Aus solchem Code
rückwärts die eigentliche Operation rauszuporkeln ist Schwerstarbeit.
Vor langer Zeit bin ich mal dem GNU Superoptimizer (oder so ähnlich)
begegnet, der alle Befehlssequenzen nacheinander durchzuprobieren
scheint, um die Beste für eine kurze Operation zu finden.
Danke!
Ja, es sind in der Tat die Inline-Assembler-Aufrufe, die offensichtlich
auf globale Variable ein Reload erzwingen. Möglicherweise müssen die
zwar nicht "asm volatile" sein (wie sie es derzeit sind), aber auch
ohne das "volatile" ergibt sich der gleiche Code. Ich hätte erwartet,
dass der Inline Assembler nur dann das Reload triggert, wenn man
explizit "memory" in der Clobber-Liste angibt.
Vielleicht hat Johann ja eine Idee.
p.s.: Habe auch mal das Umschreiben in eine static inline-Funktion
ausprobiert, die mit den Attributen "const" und "pure" deklariert
ist. Hat leider auch keine Veränderung gebracht.
Mich würde das wirklich freuen, weil es sich in größeren Projekten schon
läppert, was da an Codeverhau zusammenkommt. Alleine das kurze Stück
macht schon 30 Bytes aus. Das Ganze x-mal, da kommt so einiges zusammen!
Das Reinbasteln von temporären Variablen, teilweise mehreren parallel
ist halt sehr unschön. Und das Ganze passiert im Grunde unter allen
gcc-Version, die ich getestet habe inkl. der 5er.
Kurt Schluss schrieb:> Mich würde das wirklich freuen, weil es sich in größeren Projekten schon> läppert, was da an Codeverhau zusammenkommt. Alleine das kurze Stück> macht schon 30 Bytes aus.
Naja, aber siehe oben: das nächste Problem ist wieder ein ganz
anderes. Diese Analyse trifft nur auf das soeben gepostete zu,
bei dem die Fuses gelesen werden sollen, denn das ist am Ende in
Inline Assembler implementiert.
Generell ist die Variante mit der lokalen "Cache"-Variablen natürlich
immer sinnvoll, denn die zeigt dem Compiler recht eindeutig an, dass es
keine globalen Seiteneffekte geben kann.
Jörg W. schrieb:> Habe auch mal das Umschreiben in eine static inline-Funktion> ausprobiert, die mit den Attributen "const" und "pure" deklariert> ist.
Eine Funktion, die geinlint wird, als const oder pure zu deklarieren,
ist m.E. ziemlich sinnfrei.
Johann L. schrieb:> Eine Funktion, die geinlint wird, als const oder pure zu deklarieren,> ist m.E. ziemlich sinnfrei.
OK, überzeugt. :) War nur 'ne Verzweiflungstat.
Hast du eine Idee, warum das asm-Statement auch ohne "memory"-Clobber
immer ein Reload erzwingt?
Das ist nur ein Beispiel. Dass da die Fuses gelesen werden, ist dem
Zufall geschuldet. Der Effekt tritt, wie gesagt, auch an vielen anderen
Stellen auf, wo mit globalen Variablen gerechnet wird.
Ja, andere Ursachen wurden dir ja schon genannt: für eine externe
Funktion kann der Compiler (ohne weitere Deklarationen wie eben die
genannten Funktions-Attribute) nicht wissen, ob diese eine globale
Variable verändern oder nicht. Das ist einer der Nachteile der Sprache
C. Daher ist er dann gezwungen, den Fall anzunehmen, die aufgerufene
Funktion hätte ihn geändert, und muss den Wert neu laden. (Peter,
probier's mit dem IAR, würde mich wundern, wenn das dort grundlegend
anders wäre.)
Das Cachen in einer lokalen Variablen ist dagegen immer eine Abhilfe,
denn dann ist für den Compiler völlig klar, dass die gerufene Funktion
deren Wert nicht geändert haben kann.
Jörg W. schrieb:> Hast du eine Idee, warum das asm-Statement auch ohne "memory"-Clobber> immer ein Reload erzwingt?
Kann ich so nicht nachvollziehen. Erst wenn in folgendem Code der
Vergleich einkommentiert wird, gibt es zusätzliche globale Zugriffe
1
#include<avr/io.h>
2
3
staticinlineuint8_tf(void)
4
{
5
uint8_tx;
6
__asm__volatile__("; =%0,%=":"=r"(x));
7
returnx;
8
}
9
10
externuint8_tX;
11
12
voidX_init(void)
13
{
14
X=0;
15
X+=f();
16
//if (X != 10)
17
X-=f();
18
X*=f();
19
}
Unabhängig davon finde ich eine getChecksum() besser als eine
initChecksum().
Blah Blubb
und wieder gelöscht
Peter D. schrieb:> Jörg W. schrieb:>> Naja, das solltest du schon näher erläutern.>> Z.B. hier sind die 2 RJMP überflüssig:void foo( uint8_t val )> {> uint8_t i = 45;> if( val & 0x08 )> 44: 83 ff sbrs r24, 3> 46: 02 c0 rjmp .+4 ; 0x4c <foo+0x8>> 48: 8b e7 ldi r24, 0x7B ; 123> 4a: 01 c0 rjmp .+2 ; 0x4e <foo+0xa>> 4c: 8d e2 ldi r24, 0x2D ; 45> i = 123;> PORTB = i;> 4e: 88 bb out 0x18, r24 ; 24> }> 50: 08 95 ret
Woher weiß PeDa das? Ist bisher nicht veröffentlicht. Nur für Dich,
Jörg!
Dieter F. schrieb:> Woher weiß PeDa das?
Dieter, bitte bleib mal bei dem Code des TE, über den wir zuletzt
diskutiert haben. Das von PeDa war abgehakt bzw. gehört, wenn schon,
in einen eigenen Thread.
Das Problem des TEs ist ein anderes, aber dafür muss man erstmal den
ganzen Thread lesen.
Jörg W. schrieb:> as ist richtig, aber seit 15:51 Uhr ist er es, und seitdem haben wir> genau darüber diskutiert.
Nein, diskutiert wurde vorher schon (für einige) - und ich finde es
nicht richtig, das andere ausgeschlossen wurden.
Ist aber egal, ich werde das bei Andreas vorbringen und die Antwort
abwarten. Wenn er das für korrekt hält bin ich halt raus ...
Johann L. schrieb:>> Hast du eine Idee, warum das asm-Statement auch ohne "memory"-Clobber>> immer ein Reload erzwingt?>> Kann ich so nicht nachvollziehen.
Schade auch. Ich hatte ein bisschen drauf gehofft, dass du dir auf
die diversen Zwischenschritte, die der GCC im Debugmodus ausgibt,
einen Reim machen könntest.
Als Fazit für den TE bleibt meiner Meinung nach, dass es immer sinnvoll
ist, sowas in lokalen Variablen zwischenzuspeichern.
Jörg W. schrieb:> Johann L. schrieb:>>> Hast du eine Idee, warum das asm-Statement auch ohne "memory"-Clobber>>> immer ein Reload erzwingt?>>>> Kann ich so nicht nachvollziehen.>> Schade auch. Ich hatte ein bisschen drauf gehofft, dass du dir auf> die diversen Zwischenschritte, die der GCC im Debugmodus ausgibt,> einen Reim machen könntest.
Momentan hab ich keinen Nerv, darin rumzuwühlen. Jedenfalls hats's nix
mit asm und nix mit den volatiles und nix mit Aliasing zu tun,
vergleiche den Code per
Wahrscheinlich irgendwo in PHI, PRE, FRE, CSE (nebst Target-Kosten) den
Target-unabhängigen Passes. Mit den Beispielen sollte zumindest der
Pass, wo die Optimierung erwartet wird, auffindbar sein.
Z.B. per -fdump-tree-all-details
Kurt Schluss schrieb:> ich verwende avr-gcc zur Kompilierung für einen Atmega.>> Ich habe jetzt schon mehrfach beobachtet, dass es den Code aufbläht,> wenn man mit globalen Variablen rechnet, anstatt mit temporären, lokalen> Variablen, die man dann am Ende der globalen Variable zuweist.>> Warum ist das so? Sollte der Compiler nicht eigentlich sowas automatisch> optimieren? Ich finde, es macht die Sache auch immer etwas> unübersichtlich.> Wenn die globale Variable volatile wäre, wäre es das ja einleuchtend.> Ist sie aber in dem Fall gar nicht mal.
Globale Variablen sind igitt. Darum haben sich die Compilerentwickler
keine Mühe gegeben, den Compiler für solchen Quark zu verbessern. Die
haben genug damit zu tun, Optimierungen für guten Code zu entwickeln.
;-)
Sheeva P. schrieb:> Darum haben sich die Compilerentwickler keine Mühe gegeben, den Compiler> für solchen Quark zu verbessern.
Nö, das Problem dabei ist nur, dass man gemäß den Regeln von C sich
eben damit in vielen Fällen gar keine Mühe geben kann.
(Das letzte Beispiel habe ich mir aber noch nicht angesehen.)
Flags ist eine globale Variable, die dieses Mal nicht in einer
temporären Variable zwischengespeichert wird! Mir ging es diesmal nur
darum, zu zeigen, dass das selbe Verhalten auch bei static auftritt.
Aaaber natürlich könnte man auch Flags zwischenspeichern! Kannst es ja
mal probieren. Bringt auch wieder einige Bytes. Leider wird der Code
dann langsam aber sicher unleserlich... und genau das ist, was mir nicht
gefällt.
Kurt Schluss schrieb:> Mir ging es diesmal nur darum, zu zeigen, dass das selbe Verhalten auch> bei static auftritt.
static ist nicht das Thema, sondern Flags.
Kurt Schluss schrieb:> Dann hast du dir das neue Beispiel nicht gründlich genug angeschaut.
Ja, ist auch schon spät genug. Aber allzu groß sind die Unterschiede
auch nicht.
Kurt Schluss schrieb:> Ist direkt kompilierbar und ihr könnt #if 0 einfach durch #if 1> ersetzen, um den (mir unverständlichen) Unterschied zu sehen.
Hab ich mal gemacht - mit avr-gcc 4.7.2.
Ergebnis: Die Variante mit(!) temporärer Variable ist länger.
Mit temporärer Variable (#if 1):
Hmmmmmmm. Hatte es auch mit der 4.9.3 getestet, bzw. auch damit zum
Minimalbeispiel reduziert. Dass es bei anderen Versionen gegensätzliche
Ergebnisse gibt, ist jetzt nicht gerade vertrauenserweckend. :/
Ich sehe aber gerade, dass mein Beispiel auch zum Teil Käse ist: Der
Zähler A kann bei nur einem Durchlauf (wie im reduzierten
Minimalbeispiel) nie über 1 hinaus kommen. Da ist jetzt die Frage, wie
schlau der Compiler ist und was er wegoptimiert. Normal müsste man hier
alles in ein while (1) {...} packen.
Im realen Programm macht es jedenfalls so einige Bytes aus, mit vs. ohne
tempA.
Kurt Schluss schrieb:> Dass es bei anderen Versionen gegensätzliche> Ergebnisse gibt, ist jetzt nicht gerade vertrauenserweckend. :/
Aber auch nicht weiter ungewöhnlich. Der Optimizer des GCC besteht aus
vielen Teilen und die meisten sind nicht speziell auf eine Zielmaschine
abgestimmt. Die werden deshalb auch von etlichen verschiedenen Leuten
gepflegt. Es kann folglich vorkommen, dass Änderungen an im Prinzip
maschinenunabhängigen Optimierungen bei manchen Zielmaschinen nach
hinten losgehen.
Kurt Schluss schrieb:> Hmmmmmmm. Hatte es auch mit der 4.9.3 getestet, bzw. auch damit zum> Minimalbeispiel reduziert. Dass es bei anderen Versionen gegensätzliche> Ergebnisse gibt, ist jetzt nicht gerade vertrauenserweckend. :/
Warum? Da gibt es noch diverse Knöpfchen. Fängt bei der Optimierung an:
-O, -O2, -O3, -Os
> Im realen Programm macht es jedenfalls so einige Bytes aus, mit vs. ohne> tempA.
Bei statics, die lokal in der Funktion definiert werden? Glaube ich
nicht.
A. K. schrieb:> Wer mal sehen will, auf welche skurrilen Lösungen Compiler kommen> können, der sollte sich mal das Resultat für amd64 ansehen:> andl $8, %edi> cmpb $1, %dil> sbbl %eax, %eax> andl $-78, %eax> addl $123, %eax> movb %al, PORTB(%rip)
nennt sich branchless condition, guter code