Hallo mikrocontroller.net-Gemeinde,
ich bin neulich auf etwas gestoßen, worüber ich mich nun Frage, was
besser/effizienter, programmiertechnisch Klüger, compilerfreundlicher
und "schöner" ist.
Bisher kannte ich, folgendes um aus z.B. 4 uint8_t eine uint32_t mit
Hilfe simpler shift-Operationen zu machen. Was ich in dieser Form, auch
immer tatkräftig eingesetzt habe. Beispiel als #define (wobei das
#define nicht weiter beachtet zuwerden braucht):
Auf was ich dann vor einigen Tagen gestoßen bin ist, wäre folgendes
Codebeispiel einer Funktion:
1
2
voidconvert(bytev)
3
{
4
union{
5
uint32_tc;
6
uint8_tb[4];
7
};
8
b[0]=v;
9
b[1]=0xff;
10
b[2]=0xff;
11
b[3]=0xff;
12
return(c);
13
}
So, kann mir einer der Programmierprofi hier im Forum erklären, was
besser/effizienter, programmiertechnisch Klüger,
compilerfreundlicher/verständlicher und "schöner" ist?
Grüße
Karl
Die union ist schneeler, WENN nicht der Compiler die shifts eliminiert.
Und void Funktionen können keinen uint32 zurückliefern, das nur am
Rande, je nach Prozessor kann es auch uneffektiv sein uint32 als
returnwert liefern zu müssen.
Ein 0xFFFFFF00UL|v wäre in dem speziellen Fall natürlich schlauer.
MaWin schrieb:> Die union ist schneeler, WENN nicht der Compiler die shifts eliminiert.
"Test" hat die richtige Antwort hinsichtlich Performance.
Shifts sind langsamer als Speicher (viele 8/16bit).
Shifts sind schneller als Speicher (viele 32bit).
Die union Variante ist nur geduldet, weil streng genommen unzulässig.
Hi,
also die Union Variante ist etwas besser lesbar. Letztendlich hängt es
aber von der Anwendung ab.
Aber warum sollte dieser Code unzulässig sein?
Gruß
Rudi schrieb:> Aber warum sollte dieser Code unzulässig sein?
Unzulässig nicht, die union gehört zum C Sprachschatz.
Aber es ist nicht definiert, auf welcher Stelle die Werte liegen, ob sie
überlappen und welches bytes im long dann wo steht.
Rudi schrieb:> also die Union Variante ist etwas besser lesbar. Letztendlich hängt es> aber von der Anwendung ab.> Aber warum sollte dieser Code unzulässig sein?
Unzulässig nicht, aber nicht generell portablel. Je nach
Prozessorarchitektur kann das Ergebnis unterschiedlich sein. Kommt drauf
an, in welcher Reihenfolge normalerweise Low- und High-Byte im Speicher
landen.
Das Shifts bei 8-Bit-Prozessoren langsamer sind als das Umkopieren, ist
meistens so, weil die kleinen Dinger in der Regel keinen Barrel-Shifter
haben.
Trotzdem kommt es drauf an, was der Compiler draus macht, denn Shiften
um 8 Positionen wird meistens als Umkopieren von Registerinhalten
realisiert, und Shiften um 4 Positionen oft mit dem Swap-Nibble-Befehl,
der bei AVR zum Beispiel auch nur einen Takt braucht.
MaWin schrieb:> Rudi schrieb:>> Aber warum sollte dieser Code unzulässig sein?>> Unzulässig nicht, die union gehört zum C Sprachschatz.>> Aber es ist nicht definiert, auf welcher Stelle die Werte liegen, ob sie> überlappen und welches bytes im long dann wo steht.
Zusätzlich sagt ISO-C, dass ein Union ausschließlich gelesen wie
geschrieben werden darf. Wenn ich union.a schreibe, darf ich nur und
ausschließlich union.a lesen, nicht union.b, was dann undefined
behaviour ist.
Marian B. schrieb:> Zusätzlich sagt ISO-C, dass ein Union ausschließlich gelesen wie> geschrieben werden darf. Wenn ich union.a schreibe, darf ich nur und> ausschließlich union.a lesen, nicht union.b, was dann undefined> behaviour ist.
Was für ein ISO-C meinst du? C99 kann es nicht sein, dort steht auf
Seite 73 unter Fussnote 83:
http://www.open-std.org/jtc1/sc22/WG14/www/docs/n1256.pdf
1
If the member used to access the contents of a union object is not the same as the member last used to
2
store a value in the object, the appropriate part of the object representation of the value is reinterpreted
3
as an object representation in the new type as described in 6.2.6 (a process sometimes called "type
4
punning"). This might be a trap representation.
Schon bevor diese Fussnote eingefügt wurde, wurde in Annex J1 unter
"unspecified behavior" aufgelistet:
"The value of a union member other than the last one stored into"
"unspecified behavior" ist aber was völlig anderes als "undefined
behavior".
Marian B. schrieb:> In MISRA-C gibt's dann noch ne Regel, die entsprechende Zugriffe> verbietet.
MISRA-C 2004, Regel 18.4 sagt zuerst mal, das unions überhaupt für
garnichts nicht genommen werden sollen. Und dann macht es Ausnahmen,
falls es doch mal schnell gehen soll und man das Verhalten gut
dokumentieren kann. Unter "Packing and unpacking data" steht dann dies
als Beispiel für u.U. akzeptablen Code:
Also kann man sagen es ist "schlechter Stil" die union-Variante zu
verwenden und es sollte der shift-Variante der Vorzug gegeben werden, da
diese definiert macht was man erwartet.
Vielen dank euch allen noch, für die aufklärenden Worte.
Portabel
Da wirst du mit der Union spätestens dann Probleme bekommen, wenn der
Prozessor kein uint_8 kennt. Da ist z.B. bei der TI 28er DSP Reihe so.
Ein char oder unsigned char hat immer 16 Bit. Auch der Speicher ist 16
bittig für jede Adresse.
Das Shiften würde auch auf dem TI Prozessor funktionieren, aber nicht
die Union.
greg schrieb:> Was ist eigentlich mit der "Pointer-Casting-Variante"?
Ist sauber, wenn bei dem Cast ein Pointer auf einen char Typ rauskommt.
Andernfalls kann einem der Optimizer ein Schnippchen schlagen (gcc:
siehe -fstrict-aliasing).
Er darf nämlich davon ausgehen, dass Pointer verschiedener Basistypen
nicht auf das gleiche Objekt zeigen - ausgenommen bei char. Und dann
kann es passieren, dass er, nach Änderungen an Daten, beim Zugriff über
den falschen Pointer die vorherigen Daten liefert.
Sauber ist also:
uint16_t var;
uint8_t *p = (uint8_t *)&var;
p[0] = ...;
p[1] = ...;
Nicht sauber ist jedoch beispielsweise:
uint8_t var[2];
uint8_t *p = (uint16_t *)var;
while (...) {
var[0] = ...;
var[1] = ...;
... = *p;
}
Das wird besonders riskant, wenn solche Daten und Pointer durch
Parameter wandern und damit anonymisiert werden.
Der zweite Fall ist zwar bei AVRs ziemlich verbreitet, bei denen 16-Bit
I/O-Register wahlweise wort- und byteweise ansprechbar sind. Aber die
sind volatile, da ist der Optimizer sowieso aussen vor.
A. K. schrieb:> (gcc:> siehe -fstrict-aliasing).>> Er darf nämlich davon ausgehen, dass Pointer verschiedener Basistypen> nicht auf das gleiche Objekt zeigen - ausgenommen bei char. Und dann> kann es passieren, dass er, nach Änderungen an Daten, beim Zugriff über> den falschen Pointer die vorherigen Daten liefert.
strict-aliasing ist fundamental kaputt bei GCC. Auf keinen Fall
benutzen. Zwar wird das was der GCC da macht vom Standard abgedeckt,
aber das Verhalten ist dennoch kaputt, du lieferst ja auch schon ein
Beispiel, wo man beim Debugging wie der Ochse vorm Berg sitzt und sich
fragt, was der da für komischen Code erzeugt.
long a;
a = 5;
*(short *)&a = 4;
Mit strict aliasing kann der GCC hier machen, was er will. a kann nach
dem Block sowohl 5 als auch 4 sein.
Marian B. schrieb:> strict-aliasing ist fundamental kaputt bei GCC. Auf keinen Fall> benutzen. Zwar wird das was der GCC da macht vom Standard abgedeckt,
Nette Aussage - der GCC ist also fundamental kaputt, weil er sich am
Standard orientiert?
Im Grund behauptest du hier, dass die C Standards fundamental kaputt
sind, weil sie deinen Programmierstil nicht decken. ;-)
Dieser Aspekt der C Definition und der Implementierung in GCC hat schon
seinen Sinn. Denn jegliches Aliasing berücksichtigen zu müssen ist ein
böses Hindernis bei Codeoptimierung. Soll heissen, es macht Code
deutlich langsamer. Und du willst doch wohl, dass dein Linux-PC, deine
NAS-Box und dein Handy möglichst schnell sind, oder?
A. K. schrieb:> Im Grund behauptest du hier, dass die C Standards fundamental kaputt> sind, weil sie deinen Programmierstil nicht decken. ;-)>> Dieser Aspekt der C Definition und der Implementierung in GCC hat schon> seinen Sinn. Denn jegliches Aliasing berücksichtigen zu müssen ist ein> böses Hindernis bei Codeoptimierung. Soll heissen, es macht Code> deutlich langsamer. Und du willst doch wohl, dass dein Linux-PC, deine> NAS-Box und dein Handy möglichst schnell sind, oder?
Du kennst die Debatte ja wahrscheinlich. Worst-Case Annahmen über
Aliasing sind natürlich hinderlich bei der Optimierung. Aber nur weil
der Standard (m.E. unsinngerweise) es erlaubt, braucht der Compiler
nicht hingehen und Dinge, die sich eindeutig aliasen (wie z.B. a und
*(short*)&a) als nicht-alias zu betrachten. M.E. ist der Sinn der
entsprechenden Regelungen im Standard eher dem Compiler zu erlauben
davon auszugehen, dass etwas kein Alias ist, wenn er ist nicht wirklich
wissen kann. Hier kann er es aber wissen.
Marian B. schrieb:> Du kennst die Debatte ja wahrscheinlich.
Nein, nicht wirklich.
> Aliasing sind natürlich hinderlich bei der Optimierung. Aber nur weil> der Standard (m.E. unsinngerweise) es erlaubt, braucht der Compiler> nicht hingehen und Dinge, die sich eindeutig aliasen (wie z.B. a und> *(short*)&a) als nicht-alias zu betrachten.
Bei Programmiersprachen bin ich ein Freund formaler Spezifikationen. Und
dieses "wissen kann" in der Sprachdefinition formal zu erfassen
erscheint mir nicht als trivial.
Wenn du GCC dafür kritisierst, dass er mehr optimiert, als bei
Mikrocontrollern gesund sein mag... Tja, dafür wurde und wird er nicht
primär entwickelt.
Und wen das nervt, der hat eben -fno-strict-aliasing.
A. K. schrieb:> Wenn du GCC dafür kritisierst, dass er mehr optimiert, als bei> Mikrocontrollern gesund sein mag... Tja, dafür wurde und wird er nicht> primär entwickelt.
Ich hab bisher noch nicht beobachtet, daß er Programme kaputtoptimiert.
Mit konstanter Regelmäßigkeit folt auf "das ist kaputt in GCC" oder
"Optimierung muß für dieses Modul und GCC deaktiviert werden" falscher,
d.h. nicht standardkonformer Code.
- Type Punning und und Strict Aliasing
- Falsche Verwendung von float / double
- Erzeugung misalignter Zeiger duch Casts
- Falsche Synchronisation (Atomarität, volatile, keine
Synchronisationsprimitive, ...)
Hack wie bei *(short *)&a wird schlichweg nicht benötigt, denn memcpy
tut das ganze Standarkonform und es wird vom GCC auch optimiert (es sei
denn man nummt -ffreestanding oder -fno-builtin oder -O0).
Johann L. schrieb:> Ich hab bisher noch nicht beobachtet, daß er Programme kaputtoptimiert.
Doch, immer mal wieder hält er sich nicht an die wichtigste aller
Vorgaben: Do what I mean, not what I say!
Beim Umgang mit Wertebereichsüberlauf und ähnlichen Spässen - eine
Diskussion, die wir hier ja auch schon hatten - kommt auch immer wieder
Mal Kritik auf. Erstens dass er sich nicht so verhält, wie der
Programmierer intuitiv annimmt (ebd. DwImnwIs), zweitens weil er selbst
dann nicht drauf hinweist, wenn er das (vermeintlich?) könnte.
> d.h. nicht standardkonformer Code.
Das Problem mit standardkonformem Code ist, dass man dafür den Standard
kennen muss. Und es nicht ausreicht, mit der eigenen Intuition zu Werke
zu gehen. Mancher updatet dann seine Intuition, andere kritisieren den
Standard.
Johann L. schrieb:>> Wenn du GCC dafür kritisierst, dass er mehr optimiert, als bei>> Mikrocontrollern gesund sein mag... Tja, dafür wurde und wird er nicht>> primär entwickelt.>> Ich hab bisher noch nicht beobachtet, daß er Programme kaputtoptimiert.
Er hat einige Optimierungen drin, die man bei einem Compiler, der auf
die Kategorie AVR, MSP430 und LPC800 abzielt, nicht unbedingt
implementieren würde. So führt Optimierung machmal auch dazu, dass eine
teure Division, die man selbst ausdrücklich vor einer critical Section
platzierte, vom Optimizer genau da hinein verschoben wird. Und man
ggf. in den Code schauen muss und irgendwelche Hacks braucht, um das zu
verhindern.
So, falls Interesse besteht, habe mal mit angehängtem Code und
verschiedenen Compilern experimentiert:
1. ARM, gcc 4.8.3, -Ofast -mthumb -mcpu=cortex-m0plus
Generiert sehr ähnlichen Code für alle drei Fälle, immer Shifts und
bitwise ORs. Wenn man -Os als Optimierung verwendet, generiert der gcc
erstaunlicherweise für die Casting-Variante deutlich schlechteren
(längeren) Code!
2. x86_64, gcc 4.7.3, -Ofast
Unions generieren sehr umständlichen Code, Shifts sind besser, Casting
ist sehr kurz und prägnant.
3. 8051, sdcc 3.1.0
Hier mal ein Beispiel von einem schlechter optimierenden Compiler, der
mehr Händchenhalten braucht. Der Code für die Shift-Variante ist sehr
lang, da er praktisch ohne Tricks das umsetzt, was im Sourcecode steht.
Der 8051er hat keinen Barrel Shifter, und so wird hier eine ganze Menge
Code nur für das Shiften erzeugt. Ca. 50 Instructions!
Die Union- und Casting-Variante generieren aber guten, identischen Code.
greg schrieb:> Der 8051er hat keinen Barrel Shifter, und so wird hier eine ganze Menge> Code nur für das Shiften erzeugt. Ca. 50 Instructions!
Mit Shifts oder mit Moves? GCC/AVR setzt die Shift-Variante völlig ohne
Shifts um, aber da er das mit den Shifts natürlich einzeln macht ist es
trotzdem ziemlich umständlich.
Allerdings hast du da einen kleinen Fehler drin, denn
(y << 8)
wird als "int" ausgewertet und dann auf 32 Bits vorzeichenerweitert.
Folglich kopiert sich bei 8/16-Bittern ein gesetztes Bit 7 von "y" in
die Bits 16..31 vom Ergebnis.
Besonders elegant ist der Code bei ARM-Original und Thumb2. Die
Cast-Variante reagiert recht sensibel auf die exakte Einstellung des
Optimizers.
PS: Wenn man sowas "in gross" macht, also beispielsweise auf aktuellen
x86 oder Cortex-A, ist jede Variante, die Bytes wirklich in Speicher
schreibt und sofort in voller Breite ausliest übrigens unabhängig von
der Anzahl Befehle eine nackte Katastrophe. Weil deren Cores das nur
sehr ineffizient umsetzen und man eine zweistellige Anzahl Stall-Zyklen
riskiert.
A. K. schrieb:> Mit Shifts oder mit Moves? GCC/AVR setzt die Shift-Variante völlig ohne> Shifts um, aber da er das mit den Shifts natürlich einzeln macht ist es> trotzdem ziemlich umständlich.
sdcc hat auch diese Optimierung. Er macht aus den Shifts Moves, da es ja
byteweise Shifts sind. Aber für jeden Shift einzeln für alle 32 bit, und
dann verodert er die ganzen Resultate.
Der gcc scheint das beim AVR so ähnlich zu machen, aber mit weniger
Overhead. Richtig gut ist da die Union-Variante: nur 4 moves, sonst nix.
> Allerdings hast du da einen kleinen Fehler drin, denn> (y << 8)> wird als "int" ausgewertet und dann auf 32 Bits vorzeichenerweitert.> Folglich kopiert sich bei 8/16-Bittern ein gesetztes Bit 7 von "y" in> die Bits 16..31 vom Ergebnis.
Ja, das hab ich übersehen. Die anderen Shifts haben ja den richtigen
Cast. Aber es scheint nicht grundsätzlich etwas am Ergebnis zu ändern.
greg schrieb:> Aber es scheint nicht grundsätzlich etwas am Ergebnis zu ändern.
Damit meine ich natürlich den generierten Code, nicht das Resultat der
Rechnung.
greg schrieb:> Aber es scheint nicht grundsätzlich etwas am Ergebnis zu ändern.
Oh doch. Die falsche Variante ist bei AVR viel kürzer, 16 vs 26 Befehle
im Kern.
A. K. schrieb:> Oh doch. Die falsche Variante ist bei AVR viel kürzer.
Das kann ich hier nicht nachvollziehen. Der generierte Code ist etwas
anders, klar, aber er hat die gleiche Länge.
A. K. schrieb:> avr-gcc 4.7.2, -O2
Ist da nicht der Cast an der falschen Stelle? Du castest doch, nachdem
die Vorzeichenerweiterung durchgeführt wurde. Richtig müsste der Cast
wie bei den anderen stehen, z.B.:
greg schrieb:> Ist da nicht der Cast an der falschen Stelle?
Nein, das passt so. Das Ergebnis von
(unsigned)(y << 8)
wird ohne Vorzeichen erweitert.
Genauso möglich und sinnvoll wäre
((unsigned)y << 8)
Hingegen ist dein
((uint32_t)y << 8)
zumindest vom Prinzip her umständlicher als nötig.
NB: Da es hier um die implizite Rechnung als "int" geht, bzw. um die
Vermeidung einer Vorzeichenerweiterung dadurch, ist hier "unsigned"
sinnvoller als uint16_t, weil bei 32-Bittern neutral.
A. K. schrieb:> Nein, das passt so. Das Ergebnis von> (unsigned)(y << 8)> wird ohne Vorzeichen erweitert.
Nein, das haut nicht hin. Prüfe es selbst einmal nach.
A. K. schrieb:> Beachte auch, dass die Verwendung von "unsigned" hier sinnvoller ist als> uint16_t, da letzteres bei 32-Bittern den Code deutlich verschlechtern> kann.
Daher (und der Einheitlichkeit halber) habe ich ja auch uint32_t
verwendet. Was soll daran so umständlich sein? sdcc und avr-gcc haben
deswegen keinen fetteren Code produziert.
greg schrieb:> Nein, das haut nicht hin. Prüfe es selbst einmal nach.
Okay, muss korrigieren, das haut durchaus hin. Aber führt beim avr-gcc
eben zu enormem Code-Bloat; meine Variante nicht.
Wenn int 16 Bit breit ist, und man einen char 8 Bit nach links shiftet,
dann shiftet man ins Sign-Bit rein. Wahrscheinlich will der gcc das
irgendwie gesondert behandeln.
TL;DR: Die Shift-Variante ist relativ tricky durch die ganzen
Typkonversionen. ;)
greg schrieb:>> Beachte auch, dass die Verwendung von "unsigned" hier sinnvoller ist als>> uint16_t, da letzteres bei 32-Bittern den Code deutlich verschlechtern>> kann.> Daher (und der Einheitlichkeit halber) habe ich ja auch uint32_t> verwendet. Was soll daran so umständlich sein? sdcc und avr-gcc haben> deswegen keinen fetteren Code produziert.
Dass es real nicht umständlicher wurde liegt an der Optimierung des
Compilers. Weil der die Shifts durch Moves ersetzt. Jedoch ist
(uint32_t)y << n
auf 8/16-Bit CPUs komplexer als
(uint32_t)((unsigned)y << n)
es sei denn n ist konstant und anders als durch Shifts implementierbar.
In meinem obigen Absatz wollte ich darauf hinaus, dass
(uint32_t)((uint16_t)y << n)
bei 32-Bittern umständlicher als
(uint32_t)((unsigned)y << n)
werden kann, wenn der von Anfang an in 32 Bits rechnet und das Resultat
auf 16 Bits abschneidet. GCC erkennt allerdings mitunter, dass es aus
den Typen und Operationen abgeleitet nicht nötig ist. Verwendet man hier
unsigned statt uint16_t, dann stimmt es bei 8/16-Bittern und bei
32-Bittern ist der Cast faktisch funktionslos.
wat schrieb:> unsigned ist nur Kurzschreibweise für unsigned int.
Danke für den wichtigen Hinweis ;-). Aber was willst du damit sagen?
Denn "unsigned [int]" ist nicht notwendigerweise identisch mit uint16_t,
hat aber die gleiche Grösse wie "int", und genau um geht es hier.
Johann L. schrieb:> Ergebnis mit avr-gcc 4.6, 4.7 oder 4.8:
GCC/ARM 4.7.2 von CodeSourcery kennt den Trick nicht und geht über den
Speicher. Und genau das kann bei den Cortex-A die Suppe versalzen.
A. K. schrieb:> Johann L. schrieb:>> Ergebnis mit avr-gcc 4.6, 4.7 oder 4.8:>> GCC/ARM 4.7.2 von CodeSourcery kennt den Trick nicht und geht über den> Speicher. Und genau das kann bei den Cortex-A die Suppe versalzen.
Das ist aber seht seltsam, denn da ist überhaupt nix AVR-spezifisches
dabei (im Gegensatz zum Ziehen einer teuren Division über ein SEI / CLI,
was eine Eigenart von avr-gcc ist). Evtl hat es mit alignment zu tun
und man brauch __builtin_assume_aligned oder sowas?
Naja, aus verschiedenen Gründen ist oft -fno-builtins aktiv... und dann
macht der gcc natürlich kein optimiertes memcpy, sondern ruft stupide
die Library-Funktion auf.
Liegt das nur an der Standardkonfiguration des CodeSourcey-GCCs oder ist
das tatsächlich eine Macke, die immer auftritt? Builtins sind ein
ziemlich altes GCC-Feature, und wenn memcpy als Builtin aktiv ist, dann
sollte das auch benutzt werden.
Blöd ist, das er das nun zwar sauber optimiert, aber nicht merkt, dass
der Speicher nicht mehr gebraucht wird. Weshalb die Funktion insgesamt
so aussieht:
orr r0, r0, r1, asl #8
orr r0, r0, r2, asl #16
orr r0, r0, r3, asl #24
sub sp, sp, #8
add sp, sp, #8
bx lr