Forum: Compiler & IDEs Besseren Code mit GCC erzeugen


von Hagen R. (hagen)


Lesenswert?

Hi Leute,

unter bestimmten Umständen erzeugt der GCC beim Byteweisen Zugriff auf 
größere Datentypen immer unnötige Opcodes im Assembly. Gemeint sind zb. 
solche Zugriffe
1
uint8_t Test(uint32_t x) {
2
  
3
   uint8_t a,b,c,d;
4
5
   a = x >> 24;
6
   b = x >> 16;
7
   c = x >>  8;
8
   d = x;
9
10
   return(a^b^c^d);
11
}

Wir möchten also nur den 4 Bytes Datentyp "x" in seine 4 Bytes zerlegen.
Der GCC erzeugt dann ziemlichen Overhead der die ganze Operation mit 
mehr als doppelt soviel nötigen Takten durchführt.

Die Lösung sind nachfolgende inline Funktionen:
1
typedef union {
2
   uint32_t dword;
3
   struct {
4
     uint8_t a,b,c,d;
5
   };
6
} uint32_t_u;
7
8
static inline uint8_t HH8(uint32_t d) __attribute__((always_inline));
9
static inline uint8_t HH8(uint32_t d) {
10
11
   uint32_t_u r;
12
   r.dword = d;
13
   return(r.a);
14
}
15
16
static inline uint8_t HL8(uint32_t d) __attribute__((always_inline));
17
static inline uint8_t HL8(uint32_t d) {
18
19
   uint32_t_u r;
20
   r.dword = d;
21
   return(r.b);
22
}
23
24
static inline uint8_t LH8(uint32_t d) __attribute__((always_inline));
25
static inline uint8_t LH8(uint32_t d) {
26
27
   uint32_t_u r;
28
   r.dword = d;
29
   return(r.c);
30
}
31
32
static inline uint8_t LL8(uint32_t d) __attribute__((always_inline));
33
static inline uint8_t LL8(uint32_t d) {
34
35
   uint32_t_u r;
36
   r.dword = d;
37
   return(r.d);
38
}
39
40
typedef union {
41
   uint16_t word;
42
   struct {
43
     uint8_t a,b;
44
   };
45
} int_u;
46
47
static inline uint8_t H8(uint16_t d) __attribute__((always_inline));
48
static inline uint8_t H8(uint16_t d) {
49
50
   int_u r;
51
   r.word = d;
52
   return(r.b);
53
}
54
55
static inline uint8_t L8(uint16_t d) __attribute__((always_inline));
56
static inline uint8_t L8(uint16_t d) {
57
58
   int_u r;
59
   r.word = d;
60
   return(r.a);
61
}

Angewendet werden sie so:
1
uint8_t Test(uint32_t x) {
2
  
3
    return(HH8(x)^HL8(x)^LH8(x)^LL8(x));
4
}

Die Funktionen HH8(), HL8(), LH8(), LL8() beim Zugriff auf uint32_t, 
int32_t und H8(),L8() für uint16_t,int16_t.

Der erzeugte Asssmbly für normlen GCC Code (obige 1. Funktion)
1
 2074 0b68 9B01          movw r18,r22
2
 2075 0b6a AC01          movw r20,r24
3
 2076 0b6c 852F          mov r24,r21
4
 2077 0b6e 9927          clr r25
5
 2078 0b70 AA27          clr r26
6
 2079 0b72 BB27          clr r27
7
 2080 0b74 682F          mov r22,r24
8
 2081 0b76 CA01          movw r24,r20
9
 2082 0b78 AA27          clr r26
10
 2083 0b7a BB27          clr r27
11
 2084 0b7c 782F          mov r23,r24
12
 2085 0b7e BB27          clr r27
13
 2086 0b80 A52F          mov r26,r21
14
 2087 0b82 942F          mov r25,r20
15
 2088 0b84 832F          mov r24,r19
16
 2089 0b86 6727          eor r22,r23
17
 2090 0b88 6827          eor r22,r24
18
 2091 0b8a 6227          eor r22,r18
19
 2092 0b8c 862F          mov r24,r22
20
 2093 0b8e 9927          clr r25
21
 2095 0b90 0895          ret

und mit den inline Funktionen
1
 2074 0b68 DC01          movw r26,r24
2
 2075 0b6a CB01          movw r24,r22
3
 2076 0b6c 292F          mov r18,r25
4
 2077 0b6e 2827          eor r18,r24
5
 2078 0b70 3A2F          mov r19,r26
6
 2079 0b72 3227          eor r19,r18
7
 2080 0b74 8B2F          mov r24,r27
8
 2081 0b76 8327          eor r24,r19
9
 2082 0b78 9927          clr r25
10
 2084 0b7a 0895          ret

Nun noch ein Vergleich von
1
uint8_t Test(uint16_t x) {
2
  
3
   uint8_t a,b;
4
   a = x >> 8;
5
   b = x;
6
   return(a^b);
7
}
1
 2074 0b68 9C01          movw r18,r24
2
 2075 0b6a 892F          mov r24,r25
3
 2076 0b6c 9927          clr r25
4
 2077 0b6e 8227          eor r24,r18
5
 2078 0b70 9927          clr r25
6
 2080 0b72 0895          ret
1
uint8_t Test(uint16_t x) {
2
  
3
   return(H8(x)^L8(x));
4
}
1
 2074 0b68 8927          eor r24,r25
2
 2075 0b6a 9927          clr r25
3
 2077 0b6c 0895          ret


Gruß Hagen

von Dirk (Gast)


Lesenswert?

Nett, waere in der Codesammlung sehr effektiv.

von Peter D. (peda)


Lesenswert?

Man muß natürlich beachten, ob solche Verrenkungen im richtigen 
Verhältnis von Verschlechterung der Lesbarkeit und Verlängerung der 
Entwicklungszeit zu Kodeeinsparung stehen.

In der Regel wird das nicht der Fall sein.


Ich probiere auch gerne mal Optimierungen aus.
Die Lesbarkeit und Portabilität steht allerdings immer im Vordergrund.
Daher habe ich schon öfters Optimierungen rückgängig gemacht, wenn der 
Code zu verschraubt wurde oder das gesamte Programm gerade mal nur 50 
Words kleiner wurde.

Insbesonde Portabilitätsfallen (Byteorder in Unions) meide ich wie der 
Teufel das Weihwasser.


Peter

von Hagen R. (hagen)


Lesenswert?

Dem stimme ich zu, auch ich bevorzuge immer den besseren Source vor der 
Optimierung. Allerdings muß man auch zugestehen das gerade bei solchen 
Aufgaben wie oben der GCC so ziemlichen Schrott produziert. Sowas habe 
ich bisher noch von keinem Compiler gesehen, allerdings muß man dies 
relativieren

a) kenne ich keine anderen Compiler für den AVR zum Vergleich, sondern 
eher professionelle Compiler für Intel PCs oder eben ARM prozessoren für 
PocketPC oder Palm HandHelds

b) dafür das der GCC ein OpenSource Projekt ist und 0 Euro kostet kann 
man mit sowas leben, so lange man eben auch noch die Chance hat an 
wichtigen Stellen zu tricksen.

Die einfachste und effizenteste Lösung für obiges Problem wäre es ja 
gleich die entsprechende Funktion in Assembler direkt zu coden. Das 
wollte ich aber diesesmal absolut vermeiden.

Im diesem speziellen Fall ist ein Replacement von

x = d >> 24;

mit

x = HH8(d);

für mich super lesbar und erträglich. Hauptsache ist es eben das man ne 
Menge überflüssige Opcodes spart.

Interessant wäre es noch wenn der GCC Operatoren überladen kann. Dann 
könnte man speziell für konstante Rechtsshifts an Bytegrenzen diese 
Operatoren überladen und man würde keinerlei Unterschiede im Source mehr 
sehen.

Naja, ich bin mit obigen inlines zufrieden. Und sie erzeugen ja immer 
noch nicht den effizientesten Code der machbar wäre.

Per hand würde aus
1
 2074 0b68 DC01          movw r26,r24
2
 2075 0b6a CB01          movw r24,r22
3
 2076 0b6c 292F          mov r18,r25
4
 2077 0b6e 2827          eor r18,r24
5
 2078 0b70 3A2F          mov r19,r26
6
 2079 0b72 3227          eor r19,r18
7
 2080 0b74 8B2F          mov r24,r27
8
 2081 0b76 8327          eor r24,r19
9
 2082 0b78 9927          clr r25
10
 2084 0b7a 0895          ret

ja
1
  eor  r24,r22
2
  eor  r24,r23
3
  eor  r24,r25
4
  clr  r25
5
  ret

entstehen. Also nochmal kompakter. Ohne die Inlines also schon ein 
beträchtlicher Overhead der durch den GCC produziert wird.
1
 2074 0b68 9B01          movw r18,r22
2
 2075 0b6a AC01          movw r20,r24
3
 2076 0b6c 852F          mov r24,r21
4
 2077 0b6e 9927          clr r25
5
 2078 0b70 AA27          clr r26
6
 2079 0b72 BB27          clr r27
7
 2080 0b74 682F          mov r22,r24
8
 2081 0b76 CA01          movw r24,r20
9
 2082 0b78 AA27          clr r26
10
 2083 0b7a BB27          clr r27
11
 2084 0b7c 782F          mov r23,r24
12
 2085 0b7e BB27          clr r27
13
 2086 0b80 A52F          mov r26,r21
14
 2087 0b82 942F          mov r25,r20
15
 2088 0b84 832F          mov r24,r19
16
 2089 0b86 6727          eor r22,r23
17
 2090 0b88 6827          eor r22,r24
18
 2091 0b8a 6227          eor r22,r18
19
 2092 0b8c 862F          mov r24,r22
20
 2093 0b8e 9927          clr r25
21
 2095 0b90 0895          ret

Das ist schon'ne Menge.

Gruß hagen

von Daniel M. (usul27)


Lesenswert?

Die Frage, die ich mir an dieser Stelle allerdings stelle: Wenn 
byteweiser Zugriff auf diesen 4-Byte-Block erforderlich ist, dann ist 
das wohl von sich aus eher ein "struct" statt ein uint32_t.
Dann könnte man gleich einen Pointer auf ein struct übergeben, dann 
würde der Compiler das von sich aus sehr gut optimieren.

Oftmals bildet man sich beim Programmieren halt ein, dass mit mit einer 
32-bit-Zahl arbeitet, zerlegt die dann aber eh in einzelne Bytes. Dann 
ist struct eh der richtige Ansatz.

Ich hab mir das auch mal an einem Beispiel angeschaut:
 http://www.matuschek.net/avr-codeoptimierung-struct/

Generell empfiehlt auch Atmel mit struct zu arbeiten, weil die x,y und 
z-Register des AVR hier ideal genutzt werden können.

von Daniel M. (usul27)


Lesenswert?

P.S. In deinem Fall einer function test(uint32 t), würde die Übergabe 
eines struct-Pointers sogar noch zusätzlich sparen, da der nur 2 Byte 
gross ist im Gegensatz zu den 4 Byte der uint32_t (zusätzliche PUSH und 
POP-Operationen).

von Hagen R. (hagen)


Lesenswert?

@Daniel:

>>Die Frage, die ich mir an dieser Stelle allerdings stelle:
>>Wenn byteweiser Zugriff auf diesen 4-Byte-Block erforderlich ist,
>>dann ist das wohl von sich aus eher ein "struct" statt ein uint32_t.

Vorsicht, das ist so nicht richtig. Schau mal hier rein
 Beitrag "GCC inline assembler"

>>P.S. In deinem Fall einer function test(uint32 t), würde die Übergabe
>>eines struct-Pointers sogar noch zusätzlich sparen, da der nur 2 Byte
>>gross ist im Gegensatz zu den 4 Byte der uint32_t (zusätzliche PUSH und
>>POP-Operationen).

Nicht ganz richtig. Ein uint32_t wird in 4 Registern übergeben. Ein 
Zeiger auf eine struct in 2 Registern. Allerdings muß, damit man 
überhaupt eine Referenz auf eine Struct = Zeiger möglich ist, diese 
Struct im SRAM liegen. Das kostet dann zwar weniger in unserer 
Unterfunktion aber wesentlich mehr Aufwand in der aufrufenden Funktion. 
Und das wäre sehr schlecht. Denn ich preferiere bei einem Design einer 
Bibliothek/Software immer das TopDown Design. Dh. nicht der Caller soll 
sich um alles kümmern und den Ballast tragen sondern die aufgerufene 
Unterfunktion. Denn wenn dies so ist dann besteht unsere Software quasi 
nur noch aus kurzen und effizient knappen Aufrufen von Unterfunktionen 
in Main().Und in den Unterfunktionen, die natürlich mehrfach im Programm 
benutzt werden, steckt die Last. Das führt effektiv zum BlackBox Prinzip 
und das das erzeugte Compilat kompakter wird, das Code stärker mehrfach 
verwendet wird. Würde aber der Aufrufer einer Funktion erstmal immer 
einen Stackframe einrichten müssen dann eventuelle Daten wie uint32_t 
auf dem Stack speichern müssen damit meine Sunfunktion in der Bibliothek 
mit Zeigern arbeiten kann, dann würde man die Last quasi von der 
Bibliotheksfunktion auf den Source des Endanwenders verlagern. Hm, kein 
Design das ich bevorzuge.

Gehen wir mal davon aus: wir bekommen einen uint32_t, egal ob als 
Parameter oder Variable und er liegt logischerweise schon in Registern 
vor. Ein Typcast, ein Umkopieren  etc. in eine Struct damit wir darauf 
per Zeiger zugreifen können, wird dazu führen das der Compiler unsere 
Daten erstmal aus den Stack speichern muß. Erst danach kann er eine 
indirekte Referenz darauf anlegen.

Unter ungünstigesten Umständen muß der Compiler also erstmal einen 
Stackframe anlegen, das kostet, dann unseren uint32_t auf dem Stack 
speichern und kann dann erst einen Zeiger darauf erzeugen. Hm, nicht 
sehr clever also.

Nun zur Empfehlung von Atmel: Du vergleichst da Äpfel mit Birnen. Bei 
den obigen Funktionen geht es nicht um die Optimierung von Zugriffen auf 
Speicherstruktureb sondern auf Register durch den GCC. Der GCC 
expandiert bei Formeln die Datentypen auf den größten verwendeten 
Datentyp. Bei einen Rechtsshift von einem uint32_t mit Resulat eines 
uint8_t wird er also auch virtuell ein temporäres Resulat von uint32_t 
erzeugen und erst danach auf uint8_t als Resultat stutzen.
Das kommt in etwa der Expansion aller Datentypen in einer Formel auf 
immer den größten Datentyp gleich und der Reduktion auf die Größe des 
Resultates erst ganz am Schluß der Berechnung. Das ist zb. im Gegenteil 
zu den PASCAL Compilern. Diese versuchen von vornherein die gesammte 
Formel auf die Datengröße des Resultates abzugleichen. Wenn also das 
Resulat ein uint8_t ist und man als Input ein uint32_t benutzt in der 
Formel, dann versucht der Compiler innerhalb dieser Formel (Opcodes), 
sehr frühzeitig auf uint8_t Berechnungen zu reduzieren. Das macht er 
indem er die Formel so in der Reihenfolge ihrer Abarbeitung umstellt das 
TopDown vom größten zwingend zu benutzenden Datentyp hin zu 
kleinstmöglichen umgestellt wird. Er kann dann schon innerhalb der 
Formel sukzessive die Datentypgrößen reduzieren. So ist es zumindestens 
bei den Borland Compilern. In unserem Beispiel mit dem Rechtshift eines 
uint32_t um Bytegrenzen würde der Compiler so erkennen das er direkt auf 
eines der 4 Register in denen der uint32_t gespeichert wurde zugreifen 
kann ohne weiteres ausführen zu müssen.

Nun, und das Verhalten der "Expansion auf den größten Datentypen und 
Reduktion auf den Resulatdatentyp erst ganz am Ende" erzeugt im Compilat 
des GCC die vielen MOVs und CLRs von Registern die aber unnötig sind. Da 
kann man sich mit Typcast's verbiegen hilft alles nichts. Erst "Typcast" 
per Unions/Structs reduzieren zumindest teilweise diese Anzahl an 
Opcodes.

So zumindest verstehe ich die ganze Sache und ich kann mich natürlich 
täuschen. Für mich heist das das der Formel-optimierer des GCCs nicht 
der cleverste ist. Das kann ich absolout verstehen, denn gerade bei 
OpenSource zählt für den Programmierer "Hauptsache er rechnet die Formel 
erstmal nur richtig, optimieren können wir wenn wir Zeit dazu haben". 
Das ist nur menschlich und so gehe ich auch vor.

Gruß Hagen


von Daniel M. (usul27)


Lesenswert?

Ich verstehe schon dein Problem. Nur ist das, was du übergibts für mich 
trotzdem kein uint32_t. Wenn ich mit Integern etwas mache, dann rechne 
ich damit, aber mich interessiert nicht das 3. Byte davon. Ausnahmen 
wären lediglich sehr hardware-nahe Sachen wie der Transport dieses 
Wertes über einen 8-bit-Kanal. Bei einem float würde wohl niemand auf 
die Idee kommen, auf ein einzelnes Byte zuzugreifen - obwohl das 
natürlich auch möglich wäre.

Wenn doch, dann sind die einzelnen Bytes ja wohl irgendwie unabhängig 
voneinander. Und wenn dem so ist, dann würde ich diese entweder als 
struct oder als 4 Parameter übergeben. Damit hilfst du nicht nur dem 
Compiler sondern es ist im Code auch klarer, dass es sich hierbei nicht 
um einen Integer-Wert handelt, sondern eher um einen 4-Byte-Datenblock.
In diesem Fall ist es auch völlig egal, ob die Architektur ein 
Big-Endian oder ein Little-Endian ist, was bei deiner Implementierung 
nicht der Fall ist.

Generell ist es schon richtig, dass der GCC nicht in jedem Fall den 
idealsten Code erzeugt - so auch hier. Meine Optimierung würde aber halt 
anders aussehen. Vielleicht hätte sie auch ein paar Takte mehr als 
deine.

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.