der bei angeschalteter Optimierung automatisch aktiviert wird.
Durch
1
-fno-split-wide-types
kann der entsprechende Optimierungs-Pass in gcc deaktiviert werden.
Mit interessiert nun, welche Ergebnisse bei euch mit bzw. ohne diese
Optimierung erzielt werden, und welche Verbesserungen mit/ohne diese
Optimierung in "echten" Projekten beobachtet werden.
Hintergrund der Frage ist, ob es sinnvoll ist, den entsprechende
Optimierungs-Pass für avr-gcc zu deaktivieren. Mit einem expliziten
-fsplit-wode-types wäre er immer nocht per Kommandozeile aktivierbar.
Eine ggf. Umstellung würde frühestens in avr-gcc 4.7.0 erfolgen, dessen
Release ca. Frühjahr 2012 sein dürfte.
Da ich selbst keine allzu große Code-Basis habe, bin ich an
Erfahrungsberichten interessiert.
Für mein letztes Projekt, übersetzt mit avr-gcc 4.5.0, ergibt sich ein
Unterschied von 14960 Bytes zu 14952 Bytes, also ein Flash-Gewinn von
schlappen 8 Bytes.
Zum Hintergrund: Der Schalter bewirkt, daß ein Optimierungs-Pass läuft,
der versucht, große Typen wie 32-Bit Variablen in 8-Bit-Stücke
aufzuteilen. Je nach Code gelingt das für die gesamte Lebensdauer eines
Bytes, und wenn z.B. nachgewiesen werden kann, daß das Byte nicht
benötigt wird, können entsprechende Befehle entfernt weden.
Problem bei avr-gcc ist, daß viele Operationen nicht auf Byte-Große
zerlegt werden können. Z.B. kann eine 32-Bit-Addition nicht in 4
Teiladditionen zerlegt werden: die interne Darstellung einer 32-Bit
Addition in avr-gcc verwendet 32-Bit Objekte.
Dies führt dazu, daß die 32-Bit Objete/Operationen teilweise in 8-Bit
Teile zerlegt werden, teilweise nicht, und es einen Mix dieser beiden
Darstellungen gibt, die dem avr-Backend weitere Optimierungen
erschweren.
Ohne das Zerbröseln in 8-Bit Subtypen hat der avr-Teil in gcc etwas mehr
Möglichkeiten, selbst lowlevel-Optimierungen vorzunehmen. Das Potential
hierzu ist allerdings nicht ausgeschöpft -- vornehmlich auch deshalb,
weil -fsplit-wide-types standardmässig aktiviert ist, und das avr
Backend teilweise ganz andere Pattern zu sehen bekommt abhängig davon,
ob der Schalter aktiv ist oder nicht.
Wie sieht's also in euren Projekten aus, wenn ihr die Codegüte mit/ohne
diesen Schalter vergleicht? Am aussagekräftigsten sind natürlich
Berichte mit relativ jungen avr-gcc Releases wie 4.5.x oder 4.6.x
Johann
Ich habe gerade ein kleines C++-Programm zur Hand (hauptsächlich etwas
Gefummel mit LCD-Ausgabe); das hat mit und ohne -fsplit-wide-types
gleichermaßen 6704 Bytes (.text) bei -Oc.
Bei -O3 hat es jeweils 13010 Byte, mit -O2 jeweils 13020 Bytes
und mit -O1 jeweils 12986 Bytes.
Effekt also exakt: 0
-fsplit-wide-types wird standardmässig gesetzt; es nochmals explizit
hinzuschreiben hat also keinen Effekt.
Was Effekt hat, ist den Schalter explizit zu deaktivieren, und das
geht mittels -fno-split-wide-types, was den Standard überschreibt.
Ich habe mal ein Stück Real-Life-Code durch alle bei mir installierten
AVR-GCC-Releases geschoben. Ein 4.6.x-Release hatte ich noch nicht und
leider auch keins bei GNU gefunden, weder auf dem FTP- noch dem SVN-
Server. Also habe ich den SVN-Trunk genommen, der hat aber schon 4.7.0
gebaut. Da aber 4.7.x gerade erst begonnen wurde, schätze ich, dass die
angegebenen Werte weitgehend dem (hoffentlich in den nächsten Tagen
veröffentlichten) 4.6.0-Release entsprechen.
1
text-Größe ungelinkt (ohne Bibliotheken) -O2
2
-fno-split-wide-types -fsplit-wide-types
3
—————————————————————————————————————————————————
4
4.3.5 3646 3646
5
4.4.2 3636 3884
6
4.5.2 3628 3918
7
4.6.0/4.7.0 3630 4076
8
—————————————————————————————————————————————————
9
10
Zum Vergleich:
11
12
—————————————————————————————————————————————————
13
4.2.4 3628
14
—————————————————————————————————————————————————
Wie man sieht, wird bei der Defaulteinstellung der Code mit jeder Ver-
sion größer, mit -fno-split-wide-types bleibt die Größe in etwa gleich.
Heißt das etwa, dass diese Wide-Types-Optimierung die Hauptursache für
die schlechtere Codegrößenoptimierung der neueren GCC-Versionen war?
Genau diese war nämlich der Grund dafür, dass ich mich nie so richtig
von der 4.2.x-Version trennen konnte und dass ich so viele Versionen
parallel auf meinem Rechner installiert habe :)
Aber danke für den Hinweis!
Ich werde diese "Optimierung" künftig explizit ausschalten, mir bei
Gelegenheit aber noch ein paar Code-Beispiele anschauen, um besser zu
verstehen, was der Compiler da tut und wann das Ausschalten vielleicht
doch nicht ratsam ist. Falls ich dabei neue Erkenntnisse gewinne, werde
ich sie hier posten.
Mist, ich wollte eigentlich mit -Os testen, habe aber aus Versehen -O2
eingestellt gehabt. So sehen die Ergebnisse für -Os aus:
1
text-Größe ungelinkt (ohne Bibliotheken) -Os
2
-fno-split-wide-types -fsplit-wide-types
3
—————————————————————————————————————————————————
4
4.3.5 3482 3482
5
4.4.2 3484 3590
6
4.5.2 3581 3727
7
4.6.0/4.7.0 3524 3700
8
—————————————————————————————————————————————————
9
10
Zum Vergleich:
11
12
—————————————————————————————————————————————————
13
4.2.4 3438
14
—————————————————————————————————————————————————
Jetzt sind die Unterschiede nicht mehr ganz so groß, aber doch noch
erkennbar.
Ich habe das -O2 im vorherigen Beitrag in der Tabelle nachgetragen,
damit keine Missverständnisse enstehen.
Schon mal Danke für die Daten!
Yalu X. schrieb:> Ich habe mal ein Stück Real-Life-Code durch alle bei mir installierten> AVR-GCC-Releases geschoben. Ein 4.6.x-Release hatte ich noch nicht und> leider auch keins bei GNU gefunden, weder auf dem FTP- noch dem SVN-> Server.
Getagt ist die 4.6.0 noch nicht, wird noch'n bisschen dran rumgewienert.
Ist der Trunk unter
1
/trunk
dann ist der 4.6-Zweig unter
1
/branches/gcc-4_6-branch
und die 4.6.0-Release wird schlieslich gelegt nach
1
/tags/gcc_4_6_0_release
> Also habe ich den SVN-Trunk genommen, der hat aber schon 4.7.0 gebaut.> Da aber 4.7.x gerade erst begonnen wurde, schätze ich, dass die> angegebenen Werte weitgehend dem (hoffentlich in den nächsten Tagen> veröffentlichten) 4.6.0-Release entsprechen.
In der 4.7 gab's tatsächlich schon ne Kostenanpassung, die Auswirkung
auf die Codegröße hat mit Hinweisen auf ca -1%.
-4.5% ist ja schon sehr deutlich; so viel hätte ich da nicht erwartet...
> Wie man sieht, wird bei der Defaulteinstellung der Code mit jeder Ver-> sion größer, mit -fno-split-wide-types bleibt die Größe in etwa gleich.>> Heißt das etwa, dass diese Wide-Types-Optimierung die Hauptursache für> die schlechtere Codegrößenoptimierung der neueren GCC-Versionen war?
Schlecht zu sagen. Jedenfalls ist in den Dumps zu sehen, daß dieser Pass
(potentiellen) handgeklöppelten Optimierungen im avr-Backend im Wege
steht.
Ein weiterer Grund ist der neue Register-Allokator. An sich ist der ganz
ok, allerding macht das avr-Backend an vielen Stellen Implikationen an
den alten Allokator und dessen Verhalten. Z.B. wird ihm eine
Adressierungsart wie X+const angeboten (ursprünglich um ein paar
Corner-Cases in den Griff zu bekommen, die dann per adiw/ld/sbiw
umgesezt wird), die AVR überhaupt nicht hat.
Ich hab schon versucht, dem Backend das abzugewöhnen und einige neue
Hooks zu verwenden. Bei extremer Registerlast in einem sehr
longlong-lastigen Testfall steigt gcc aber aus, weil ihm die Register
ausgehen... Ansonsten kommt der Code näher an "vernünftigen" AVR-Code
ran.
> Genau diese war nämlich der Grund dafür, dass ich mich nie so richtig> von der 4.2.x-Version trennen konnte und dass ich so viele Versionen> parallel auf meinem Rechner installiert habe :)
Geht mir so mit der 3.4.6. Für meine privaten Mini-Projekte tut der am
besten.
> Ich werde diese "Optimierung" künftig explizit ausschalten, mir bei> Gelegenheit aber noch ein paar Code-Beispiele anschauen, um besser zu> verstehen, was der Compiler da tut und wann das Ausschalten vielleicht> doch nicht ratsam ist. Falls ich dabei neue Erkenntnisse gewinne, werde> ich sie hier posten.
Gerne :-)
Johann L. schrieb:> Getagt ist die 4.6.0 noch nicht, wird noch'n bisschen dran rumgewienert.> Ist der Trunk unter>> /trunk>> dann ist der 4.6-Zweig unter>> /branches/gcc-4_6-branch>> und die 4.6.0-Release wird schlieslich gelegt nach>> /tags/gcc_4_6_0_release
Ich habe zunächst nur nach /tags/gcc_4_6_0_release gesucht. Auf die
Idee, auch in den Branches nachzuschauen, bin ich erst gekommen, nachdem
ich den obigen Beitrag schon geschrieben hatte. Da mit meinem nur mäßig
schnellen Internetzugang der Checkout eine halbe Ewigkeit dauert, habe
ich es dabei bewenden lassen. Oder kann man mit Subversion einen Branch
in eine bestehende Arbeitskopie so outchecken oder updaten, dass nur
geänderte Dateien vom Server geholt werden?
Yalu X. schrieb:> Da mit meinem nur mäßig> schnellen Internetzugang der Checkout eine halbe Ewigkeit dauert, habe> ich es dabei bewenden lassen. Oder kann man mit Subversion einen Branch> in eine bestehende Arbeitskopie so outchecken oder updaten, dass nur> geänderte Dateien vom Server geholt werden?
Oje, da ist ja alles mögliche Gerüffel wie java, ada, testsuite,
libstdc++ dabei...
Eine Möglichkeit wäre svn switch.
Da sich 4.6 und 4.7 noch kaum unterscheiden, kann man auch lediglich das
avr-Backend updaten, z.B. per
1
svn update -rHEAD
in /gcc/config/avr/
Der letzte 4.7 Snapshot ist 171148, latest 4.6 Snapshot ist 171171.
Eine Hack, um schneller zu updaten, ist unter "Optimize disk usage" in
http://gcc.gnu.org/wiki/SvnSetup aufgezeigt. Dort werden nicht
verwendete Verzeichnisse ins Nirvana gemappt.
Fazit: nicht bei allen Applikationen ändert sich was, aber wenn sich
was ändert, dann ist der Code mit -fno-split-wide-types in jedem
Falle kleiner geworden.
Das Beispiel, das ich gestern gepostet habe, verwendet an einigen
Stellen FP-Arithmetik. Speziell die GCC-Version 4.7.0 scheint damit
leichte Probleme zu haben. Folgender Code
1
doubleg(doublex,doubley);
2
3
doublef(doublex,doubley){
4
returng(x,y);
5
}
braucht bei 4.7.0 mit -Os defaultmäßig 18 Bytes mit
-fno-split-wide-types nur 6 Bytes. Das ist Faktor 3 ;-)
Im Assemblercode sieht man sofort, warum:
1
f:
2
call g
3
mov r20,r22
4
mov r21,r23
5
mov r22,r24
6
mov r23,r25
7
movw r24,r22
8
movw r22,r20
9
ret
Die MOV/MOVW-Orgie tut nichts, außer unnötigerweise zwei zusätzliche
Register zu beschreiben und Taktzyklen zu verbrauchen.
Mit -fno-split-wide-types oder 4.5.2 oder %s/double/long/g sieht das
Ergbnis anständig aus:
Jörg Wunsch schrieb:> -mrelax?
Ja, das funktioniert. Allerdings wird dadurch der CALL zwar durch einen
JMP oder RJMP ersetzt, der RET bleibt aber trotzdem stehen. Es werden
also Zyklen, aber nicht unbedingt Bytes eingespart.
Dass der RET stehenbleibt, mag damit zusammenhängen, dass -mrelax nicht
vom Compiler, sondern vom Linker bearbeitet wird. Aber warum kann der
Compiler nicht selbst die CALL-RET-Kombination durch einen JMP ersetzen?
Bei Endrekursionen hat man ja fast die gleiche Situation, und die werden
direkt vom Compiler optimiert.
Yalu X. schrieb:> Jörg Wunsch schrieb:>> -mrelax?>> Ja, das funktioniert. Allerdings wird dadurch der CALL zwar durch einen> JMP oder RJMP ersetzt, der RET bleibt aber trotzdem stehen. Es werden> also Zyklen, aber nicht unbedingt Bytes eingespart.>> Dass der RET stehenbleibt, mag damit zusammenhängen, dass -mrelax nicht> vom Compiler, sondern vom Linker bearbeitet wird. Aber warum kann der> Compiler nicht selbst die CALL-RET-Kombination durch einen JMP ersetzen?> Bei Endrekursionen hat man ja fast die gleiche Situation, und die werden> direkt vom Compiler optimiert.
Ein entsprechendes Patch ist bereits abgesegnet, aber noch nicht
eingespielt. Das kann einen Tail-Call auch dann optimieren, wenn
Register gepusht werden.
Jörg Wunsch schrieb:> Ich habe mal eine einzelne Konfiguration des µracoli-Projekts> mit beiden Varianten compiliert:> [...]> Fazit: nicht bei allen Applikationen ändert sich was, aber wenn sich> was ändert, dann ist der Code mit -fno-split-wide-types in jedem> Falle kleiner geworden.
Welche gcc-Version wars?
Das kann jetzt auch avr-gcc :-)
Mit dem -fno-split-wide-types sieht's leider nicht so gut aus. In
Funktionen der libgcc wird der Code teilweise drastisch größer oder
drastisch kleiner mit dem Schalter, also ein uneinheitliches Bild. Der
größere Code ist vor allem bei 64-Bit Routinen, die nicht sooo
praxisrelevant sind... Aber das macht so eine Umstellung viel schwerer
zu begründen.
Johann L. schrieb:>>> f:>> jmp g>>> Das kann jetzt auch avr-gcc :-)
Das ging aber schnell. Ich hoffe, das Feature wurde nicht extra wegen
meines Beitrags von oben eingebaut ;-)
In welcher Version? Noch in 4.6 oder erst in 4.7?
> Mit dem -fno-split-wide-types sieht's leider nicht so gut aus.
Das ist auch nicht so schlimm. Wichtig ist es zu wissen, dass es da eine
Option gibt, bei der das Herumspielen u.U. lohnt. Die Defaulteinstellung
ist dann eher zweitrangig.
Yalu X. schrieb:> Johann L. schrieb:>>>> f:>>> jmp g>>>>> Das kann jetzt auch avr-gcc :-)>> Das ging aber schnell. Ich hoffe, das Feature wurde nicht extra wegen> meines Beitrags von oben eingebaut ;-)>> In welcher Version? Noch in 4.6 oder erst in 4.7?
In 4.7. Für ältere Versionen gibt's bestenfalls Bugfixes, aber teilweise
noch nichtmal diese (da unerwünscht).
Die Optimierung von Tail Calls ist ein neues Feature, und das kann nur
in der neuesten Version hinzugefügt werden. Die Optimierung geht über
das Peepholing der binutils hinaus.
>> Mit dem -fno-split-wide-types sieht's leider nicht so gut aus.>> Das ist auch nicht so schlimm. Wichtig ist es zu wissen, dass es da eine> Option gibt, bei der das Herumspielen u.U. lohnt. Die Defaulteinstellung> ist dann eher zweitrangig.
Naja, der Feld-Wald-und-Wiesen-Anwender hat bestimmt keine Lust und
keine Zeit, aus den hunderten von gcc-Optionen die beste Kombination
herauszuarbeiten.
Neben dieser Option sind mir z.B. auch -fno-move-loop-invariants und
-fno-tree-loop-optimize unangenehm aufgefallen. Invarianten, die in
einer Schleife stehen, werden vor die Schleife gezogen. Das erhöht
teilweise die Registerlast derart, daß ein Stackframe notwendig wird.
Oder der Wert landet zumindest in einem call-saved Register (vor allem,
wenn in der Schleife eine Funktion aufgerufebn wird, die eine Konstante
bekommt), was pushs/pops nach sich zieht oder umständliche
Befehlssequenzen, wenn eine Konstante in ein unteres Register 0..15
geschrieben werden muss.