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_tTest(uint32_tx){
2
3
uint8_ta,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:
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)
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
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.
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.
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).
@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
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.