Mir ist die Tage folgendes aufgefallen, was ich nicht erklären kann.
In folgendem simplen C-Code kann eigentlich der Zugriff auf g komplett weg-optimiert werden. Das tut gcc aber nur, wenn g definitiv sequentiell nicht geändert wird, etwa wenn bei (a) statt ++g; dann ++g;--g; geschrieben wird.
Aber das ist jetzt nicht der Punkt, wenngleich man es sich anders wünscht.
gcc zieht sinnvollerweise "nur" das Inkrement aus der Schleife und macht daraus ein g += 20; außerhalb der Schleife. Auch gut.
1
staticuint16_tg;
2
3
intmain(){
4
for(uint8_ti=0;i<20;i++){
5
++g;// <a>
6
VPORTC.DIR;// <1> OK
7
// VPORTC_DIR; // <2> suppresses optimization
8
}
9
}
1
main:
2
ldsr24,g;g_lsm.6,g
3
ldsr25,g+1;g_lsm.6,g
4
ldir18,lo8(20);ivtmp_2,
5
.L2:
6
inr19,0x8;vol.1_7,MEM[(structVPORT_t*)8B].DIR
7
subir18,lo8(-(-1));ivtmp_2,
8
cpser18,__zero_reg__;ivtmp_2,
9
rjmp.L2;
10
adiwr24,20;tmp49,
11
stsg,r24;g,tmp49
12
stsg+1,r25;g,tmp49
13
ldir24,0;
14
ldir25,0;
15
ret
Dies passiert aber nur, wenn der Zugriff auf das direction-register von PortC mit structure-mapping zugegriffen wird. Beim direkten Zugriff via VPORTC_DIR ist gcc nicht mehr in der Lage zu optimieren:
1
main:
2
ldir24,lo8(20);ivtmp_4,
3
.L2:
4
ldsr18,g;g,g
5
ldsr19,g+1;g,g
6
subir18,-1;tmp48,
7
sbcir19,-1;,
8
stsg,r18;g,tmp48
9
stsg+1,r19;g,tmp48
10
inr25,0x8;vol.1_7,MEM[(volatileuint8_t*)8B]
11
subir24,lo8(-(-1));ivtmp_4,
12
cpser24,__zero_reg__;ivtmp_4,
13
rjmp.L2;
14
ldir24,0;
15
ldir25,0;
16
ret
Das ist sehr merkwürdig.
Was geht da vor? (Ein aliasing Problem in der escape-analyse?)
Dies passiert aber nur, wenn der Zugriff auf das direction-register von
PortC mit structure-mapping zugegriffen wird. Beim direkten Zugriff via
VPORTC_DIR ist gcc nicht mehr in der Lage zu optimieren:
[...]
Das ist sehr merkwürdig.
Was geht da vor? (Ein aliasing Problem in der escape-analyse?)
Mein Gott: Sieh' einfach ein, dass Compiler potentiell suboptimal sind. Es ist, das theoretisch völlig zweifelsfrei nachweisbar, völlig unmöglich, einen zu schreiben, der in jedem Kontext den optimalen Code erzeugt, denn so ein Compiler hätte eine quasi "unendliche" Komplexität, würde also niemals ein Compilat liefern können.
Tatsächlich funktionieren Compiler nur deshalb, weil sie die Sache in begrenzte Häppchen mit entsprechend begrenzter Komplexität zerlegen. Und diese Häppchen können sie dann (mit ein wenig Glück) optimal umsetzen.
Gelaber. Hilft nicht bei der Beantwortung der Frage.
Außer für avr-gcc-Compilerbauer ist das aber die einzig richtige Antwort.
Es ist schlicht sinnlos, zu fragen, warum ein Compiler etwas so macht, wie er es macht.
Allenfalls ein bugreport könnte man erstellen, aber über die kurze Diskussion, ob das überhaupt ein bug ist, wird der auch nicht hinauskommen.
Sowas ist kein Bug, denn das Verhalten ist korrekt. Man könnte versuchen, es als missed optimization einzutüten, wird aber vermutlich feststellen, dass es einen Grund gibt, der für den Anwender des Compilers nicht offensichtlich ist.
C-hater hat insoweit Recht, als man von einem Compiler keine perfekte Optimierung etwarten kann. Korrektes Verhalten ist wichtiger.
Eine Spekulation dazu wäre, dass die Technik des Portzugriffs für den Compiler eine Absicherung gegen mögliches Aliasing auslöst. Er sich nicht völlig sicher ist, dass der Zauber hinter dem Portzugriff nicht vielleicht auf g zeigt.
Mein Gott: Sieh' einfach ein, dass Compiler potentiell suboptimal sind.
Du hast die Frage nicht verstanden! Ich bin ja mit der nicht so ganz vollständigen Optimierung des gcc im Falle (1) vollkommen zufrieden. Die Frage ist, warum wird das durch Version (2) verhindert.
Es ist schlicht sinnlos, zu fragen, warum ein Compiler etwas so macht,
wie er es macht.
Nein. Es ist wichtig zu verstehen: im Grunde ist die ein Schulbeispiel für einen Kurs im Compilerbau. Diesen Test besteht der gcc ja, wenn auch nicht vollständig. Die eigentliche Frage ist ja eine andere (s.o.).
Eine Spekulation dazu wäre, dass die Technik des Portzugriffs für den
Compiler eine Absicherung gegen mögliches Aliasing auslöst.
Die Vermutung hatte ich ja oben auch schon (s. Eröffnungspost). Ich habe es auch schon mit einem restrict-qualified Zeiger versucht - auch ohne Erfolg.
Letztendlich geht es um den Unterschied zwischen VPORTC_DIR und VPORTC.DIR in Bezug auf die Optimierung. Falls mir das jemand erklären kann, wäre ich schon zufrieden.
Sind halt Compiler-Interna. Noch dazu wurde das gesamte Backend in GCC 10 (meiner Erinnerung nach) komplett umgebaut. Du kannst also spaßeshalber auch mal einen (etwas) älteren GCC als Vergleich heranziehen. In vielen Fällen ist der alte Compiler besser, Johann hatte seinerzeit viel am AVR-Backend gemacht. Andererseits war das ganze Backend von vornherein (wenn ich mich recht erinnere) ein wenig "verkorkst", weil es einen fiktiven 16-Bit-Prozessor implementiert hat, was zu anderen missed optimizations geführt hat. Insofern bringt das neue Backend an einigen Stellen Optimierungen zustande, die das alte nicht recht konnte.
Die erste Frage ist ja bei dir immer: C oder C++ ?
Da er das "void" in der Argumentliste weggelassen hat: C++. Spielt aber hier keine Geige, ist reproduzierbar (auch mit C). Ist ja eh eine Backend-Frage und unabhängig vom Frontend.
Sind halt Compiler-Interna. Noch dazu wurde das gesamte Backend in GCC
10 (meiner Erinnerung nach) komplett umgebaut. Du kannst also
spaßeshalber auch mal einen (etwas) älteren GCC als Vergleich
heranziehen.
Bingo.
In gcc-4.5.4 existiert das Problem nicht, jedoch ab gcc-4.6.4 schon.
Ist also noch etwas älter und hat - wie schon gesagt - nichts mit dem AVR-Backend zu tun, sondern etwas mit dem Middle-End (bis RTL). Daher tritt es bei allen Targets auf, nicht nur beim AVR.
Daher tritt es bei allen Targets auf, nicht nur beim AVR.
Wenn du das natürlich auf einem amd64-Target nachvollziehbar machen kannst, hat ein "missed optimization" Bugreport gute Chancen, dass sich dessen jemand annimmt.
Die Frage ist, warum wird das durch Version (2) verhindert.
Ein Anfang wäre, den Pass zu finden, in dem die Optimierung /nicht/ stattfindet. Dazu compilieren mit -fdump-tree-all -fdump-ipa-all -fdump-rtl-all und die Dump-Dateien untersuchen und feststellen, was in (1) anders läuft als in (2). Vermutlich ist es einer der Tree-Passes.
Die Frage ist, warum wird das durch Version (2) verhindert.
Ein Anfang wäre, den Pass zu finden, in dem die Optimierung /nicht/
stattfindet. Dazu compilieren mit -fdump-tree-all -fdump-ipa-all
-fdump-rtl-all und die Dump-Dateien untersuchen und feststellen, was in
(1) anders läuft als in (2). Vermutlich ist es einer der Tree-Passes.
Danke: werde ich mir auch mal bei Gelegenheit ansehen.
Wie groß die Auswirkungen in der Praxis sind, ist fraglich. Jedenfalls trifft es natürlich auch solche Sachen wie:
1
staticuint16_tcounter;
2
3
staticuint16_tcount_3(){
4
ATOMIC_BLOCK(ATOMIC_RESTORESTATE){
5
returnACCESS_ONCE(counter);
6
}
7
}
8
staticvoidfunc(void){
9
for(uint8_ti=0;i<20;i++){
10
g+=count_3();
11
}
12
}
Da ich jedoch denke, dass oft an den betroffenen Stellen auch noch non-inline Funktionsaufrufe enthalten sind und nicht mit LTO gearbeitet wird, sind die Auswirkungen begrenzt.
Anders sieht es allerdings aus, wenn man z.B. alle Registerzugriffe konsequent über structure-mapping macht und auch sonst alles inline ist (weil etwa alles als templates in header-only Bibliotheken drin ist), weil bspw. eben C++ verwendet. Jedenfalls ist es mir so im Vergleich von meinem C++-Code zu einer reinen C-Implementierung aufgefallen, dass in C schlechter optimiert werden konnte.
Jedenfalls
trifft es natürlich auch solche Sachen wie:
"ATOMIC_BLOCK" beinhaltet eine Optimization/Compiler/Memory Barrier. Der
Compiler darf den Wert von "counter" nicht raus- oder reinziehen.
Ja, klar.
Aber darum geht es doch in diesem Beispiel gar nicht.
Hier geht es darum, dass in ATOMIC_BLOCK ja zweimal der Zugriff auf das Status-Register mit dem Macro SREG stattfindet. Und weil das so ist und mit dem Fehler in gcc ab 4.6.4. findet keine Optimierung bzgl. der globalen Variable g mehr statt. Ups, sehe gerade, dass ich die im obigen Beispiel irgendwie nicht copy-n-pasted habe. Also denke Dir bitte eine Zeile
1
staticuint16_tg;
noch dazu.
Keine Ahnung was das "ACCESS_ONCE" darin noch macht?
Das ist wie im Linux-Kernel der lvalue-volatile-cast (hier streng genommen wegen der memory-barrier unnötig). Allerdings ist es ja leider so, dass die globale MemoryBarrier (oder auch ggf. ein Output-Clobber bzgl. counter hier) fälschlicherweise auch eine Optimierung bzgl. der globalen Variable g verhindert. Also, insgesamt 3 Fehler im aktuellen gcc, wobei jeder für sich schon die Generierung eines optimalen Code verhindert.
Workaround ist hier, auf den direkten Register-Zugriff zu verzichten und stattdessen das structure-mapping einzusetzen, und auch auf die ATOMIC_BLOCK-Macros zu verzichten, weil die ja ebenfalls direkt auf SREG zugreifen und zudem eine globale Memory-Barrier benutzen.
In C++ benutze ich schon immer ein structure-mapping mit eingenen Register-Typen, so dass auch nur die richtigen Flags für die Register benutzt werden können, ansonsten Compile-Zeit-Fehler, und andererseits auch schon immer eine immer gleich aufgebaute Klasse für die ISRs mit den nebenläufig verwendeten Variablen ohne eine Memory-Barrier, sondern explizitem volatile-access außerhalb der ISR. Das führt immer zu einem optimalen Ergebnis auch in Gegenwart der drei gcc Fehler. Das habe ich auch so nicht gewusst: Glück gehabt ;-)
Wohlgemerkt, dass sind Fehler im Middle-End von gcc und haben m.E. nichts mit dem AVR Backend zu tun (im avr-gcc fällt es aber eben am meisten auf).
dem kommt es wohl sowieso nicht auf optimale Performance an.
Es mag ja Dinge geben, von denen du Ahnung hast … C auf AVRs gehört nicht dazu. (Davon abgesehen, dass wir ja schon festgestellt haben, dass das hier nicht AVR-spezifisch ist.)
(Davon abgesehen, dass wir ja schon festgestellt haben, dass
das hier nicht AVR-spezifisch ist.)
Evtl. macht es Sinn, den Titel des Threads zu ändern. Auf der anderen Seite ist das wohl auch vergeblich: zwar tritt das bei allen Targets auf, doch dort "interessiert" es ggf. weniger, bei den kleinen AVRs fällt es einfach mehr ins Gewicht.
Die BugReports für den gcc sind auch erstellt.