avr-gcc: direkter Register Zugriff vs structure-mapping und fehlende Optimierung?

OP #7349917
Lesenswert?

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
static uint16_t g; 
2

3
int main() {
4
    for(uint8_t i = 0; i < 20; i++) {
5
        ++g; // <a> 
6
        VPORTC.DIR; // <1> OK
7
//        VPORTC_DIR; // <2> suppresses optimization
8
    }
9
}
1
main:
2
lds r24,g        ;  g_lsm.6, g
3
lds r25,g+1      ;  g_lsm.6, g
4
ldi r18,lo8(20)  ;  ivtmp_2,
5
.L2:
6
in r19,0x8       ;  vol.1_7, MEM[(struct VPORT_t *)8B].DIR
7
subi r18,lo8(-(-1))      ;  ivtmp_2,
8
cpse r18,__zero_reg__    ;  ivtmp_2,
9
rjmp .L2         ;
10
adiw r24,20      ;  tmp49,
11
sts g,r24        ;  g, tmp49
12
sts g+1,r25      ;  g, tmp49
13
ldi r24,0                ;
14
ldi r25,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
ldi r24,lo8(20)  ;  ivtmp_4,
3
.L2:
4
lds r18,g        ;  g, g
5
lds r19,g+1      ;  g, g
6
subi r18,-1      ;  tmp48,
7
sbci r19,-1      ; ,
8
sts g,r18        ;  g, tmp48
9
sts g+1,r19      ;  g, tmp48
10
in r25,0x8       ;  vol.1_7, MEM[(volatile uint8_t *)8B]
11
subi r24,lo8(-(-1))      ;  ivtmp_4,
12
cpse r24,__zero_reg__    ;  ivtmp_4,
13
rjmp .L2         ;
14
ldi r24,0                ;
15
ldi r25,0                ;
16
ret

Das ist sehr merkwürdig. Was geht da vor? (Ein aliasing Problem in der escape-analyse?)

Gast #7349991
Lesenswert?

Wilhelm M. schrieb:

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.

#7350030
Lesenswert?

c-lover schrieb:

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.

Oliver

#7350037
Lesenswert?

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.

OP #7350099
Lesenswert?

c-hater schrieb:

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.

Oliver S. schrieb:

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.).

(prx) A. K. schrieb:

Sowas ist kein Bug, denn das Verhalten ist korrekt.

Es ist ganz klar ein missing-optimization-bug.

(prx) A. K. schrieb:

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.

Moderator Persönliche Seite #7350159
Lesenswert?

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.

Vielleicht meldet sich Johann ja hier noch rein …

OP #7350214
Lesenswert?

Andreas M. schrieb:

Und wir sollen jetzt raten, was sich hinter den beiden Ausdrücken verbirgt?

Wer etwas mit AVR zu tun hat, der weiß was sich dahinter verbirgt.

Hier jetzt nochmal ausformuliert:

1
typedef struct C {
2
    volatile uint8_t r; 
3
} C_t;
4

5
#define VC (*(C_t *) 0x0008) 
6

7
#define C_r  (*(volatile uint8_t *)(0x0008))
8

9
static uint16_t g; 
10

11
int main() {
12
    for(uint8_t i = 0; i < 20; i++) {
13
        ++g;
14
//        VC.r; // <1> OK
15
        C_r; // <2> suppresses optimization
16
//        VPORTC.DIR; // <1> OK
17
//        VPORTC_DIR; // <2> suppresses optimization
18
    }
19
}
OP #7350217
Lesenswert?

Jörg W. schrieb:

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.

Persönliche Seite #7350276
Lesenswert?

Wilhelm M. schrieb:

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.

OP #7350511
Lesenswert?

Johann L. schrieb:

Wilhelm M. schrieb:

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
static uint16_t counter;
2

3
static uint16_t count_3() {
4
    ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
5
        return ACCESS_ONCE(counter);
6
    }
7
}
8
static void func(void) {
9
    for(uint8_t i = 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.

OP #7352741
Lesenswert?

Εrnst B. schrieb:

Wilhelm M. schrieb:

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
static uint16_t g;

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).

OP #7357283
Lesenswert?

Jörg W. schrieb:

(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.

Antwort schreiben

Bitte melde dich an, um einen Beitrag zu schreiben.

oder

Mit Google-Account einloggen

Die Registrierung ist kostenlos und dauert nur eine Minute.

Jetzt registrieren