Hallo zusammen,
ich habe eine 16-Bit-Variable und ein 16 Bit breites Bitfeld. Die
Reihenfolge im Bitfeld entspricht der Bit-Reihenfolge unter
Berücksichtigung der Endianness.
Die Definition des Bitfelds sieht so aus:
1
typedefstructInputs_s
2
{
3
unsignedintmpgKey:1;/**< Funktionstaste am Handrad gedrueckt */
Man sieht: Es wird jedes Bit einzeln geprüft und zugewiesen, und man
sieht auch, dass der Bit-Offset immer gleich ist. Ich hätte erwartet,
dass der Compiler daraus eine Maskierung und eine Zuweisung macht.
Was hindert ihn daran?
An dieser "Mikro-Optimierung" hängt nichts. Aber ich will es trotzdem
verstehen.
MaWin schrieb:> Da musst du den Hersteller des unbekannten Compilers fragen.
Würde Dein Lieblings-Compiler das optimieren? Meinen kannst Du ja nicht
ausprobieren.
Ich habe einen ARM-GCC (none-eabi) 5.4.1 in der Optimierungsstufe -O1,
aber ich kann ja von niemandem verlangen, mein Build-System
nachvollziehen zu können.
Walter T. schrieb:> Ich hätte erwartet,> dass der Compiler daraus eine Maskierung und eine Zuweisung macht.
Macht er doch, je Bit. Besser gehts nicht.
So eine wahllose Umsortiererei ist nunmal aufwendig. Warum muß das
überhaupt sein?
Peter D. schrieb:> Warum muß das> überhaupt sein?
Meinst Du das Bitfeld? Das Bitfeld wird jetzt ein wenig durchgereicht
(call by value), deswegen bevorzuge ich es gegenüber einem großen
boolhaltigen struct. Und dann wird ein wenig boolesche Logik darauf (und
ein paar andere Bitfelder) angewendet und die Zustandsmaschine damit
gefüttert. Da ist die Bitfeld-Schreibweise einfach übersichtlicher, als
in jedem IF-Konstrukt mit der Maske zu verunden.
Ich könnte auch die bool-Variablen erst an den Endpunkten des
Durchreichens erzeugen, aber ich brauche die gleichen Werte an mehreren
Stellen, deswegen erschien mir das praktischer.
Peter D. schrieb:> So eine wahllose Umsortiererei ist nunmal aufwendig.
Wieso wahllos? Der Offset ist überall gleich. Die Bit-Reihenfolge ist im
uint16 und im Bitfeld doch gleich.
Walter T. schrieb:> Die Bit-Reihenfolge ist im> uint16 und im Bitfeld doch gleich.
Nachtrag: Das erste Makro SWITCH_KEY0 hat den Wert 1 und alle folgenden
Makros jeweils den doppelten Wert. Aber ich dachte, da sei aus dem
Assembler-Listing ersichtlich.
Walter T. schrieb:> Da ist die Bitfeld-Schreibweise einfach übersichtlicher
Das geht aber genauso gut, wenn Du die Bitreihenfolge einfach so läßt,
wie sie pushbutton_get() liefert.
Peter D. schrieb:> Das geht aber genauso gut, wenn Du die Bitreihenfolge einfach so läßt,> wie sie pushbutton_get() liefert.
Das war auch mein Plan. Wo habe ich mir da einen Fehler geleistet? Der
ARM Cortex M3 wird im little-Endian-Modus betrieben.
(Einen Fehler in der Bit-Reihenfolge auszuschließen war auch der einzige
Grund, mir das ASM-Listing überhaupt anzusehen. Aber ich sehe den
Schnitzer immer noch nicht.)
Walter T. schrieb:> Die Bit-Reihenfolge ist im> uint16 und im Bitfeld doch gleich.
Im Assembler sieht aber alles umgedreht aus:
1 -> 6
2 -> 5
3 -> 4
usw.
Walter T. schrieb:> Wieso wahllos? Der Offset ist überall gleich. Die Bit-Reihenfolge ist im> uint16 und im Bitfeld doch gleich.
Nein, sie ist umgedreht.
Vermutlich ist das einfach so ein spezieller Fall (Eine 16-Bit-Variable,
die im Prinzip einfach in eine andere 16-Bit-Variable kopiert wird,
aber nicht am Stück, sondern Bit für Bit einzeln), dass sich keiner die
Mühe gemacht hat, so eine Optimierung einzubauen. Warum nimmst du nicht
einfach einen Cast oder memcpy?
Yalu X. schrieb:> Nein, sie ist umgedreht.Peter D. schrieb:> Im Assembler sieht aber alles umgedreht aus:
Ihr habt Recht. Ich habe das falsche struct und das falsche Listing
hochgeladen. Dabei hatte ich schon meinen Kaffee. Das, was ihr oben
seht, war eine Verzweiflungstat.
Rolf M. schrieb:> Warum nimmst du nicht> einfach einen Cast oder memcpy?
Die ausführliche Schreibweise oben ist portabel. So
Optimierungs-Verzweifelt, dass ich implementationsabhängigen Code
einbauen will, bin ich an dieser Stelle nicht. Es ist ja eher ein
Kuriosum als ein echtes Problem.
Kannst ja beim GCC Bugtracker ein "missed optimization opportunity" Bug
einstellen. Hab ich vor 10 Jahren mal für eine Byte Permutation für x86
gemacht. Hat keine Woche gedauert, dann war das im GCC drin.
Hier mal ein Ausschnitt aus einer älteren Steuerung (ATmega8):
Die Eingänge werden über 74HC165 eingelesen, die Ausgänge über 74HC595
ausgegeben und die LEDs über MAX7219.
Im RAM befinden sich quasi die virtuellen Pins. Über Macros werden sie
dann so definiert, wie sie sich aus dem Schaltplan ergeben. Eine
Umsortierung ist daher nicht nötig, da immer nur mit den Symbolen
gearbeitet wird.
Peter D. schrieb:> Hier mal ein Ausschnitt aus einer älteren Steuerung (ATmega8)
Den Entwurf habe ich auch in Erwägung gezogen und an dieser Stelle
verworfen. Hier will ich mal Eingänge nicht global haben, sondern
definiert per Funktionsaufrufen übergeben. (Im Hintergrund stellt dann
eine Doppelpufferstrategie sicher, dass die Zustände jeweils konsistent
sind. Für die Eingänge selbst ist das auch gar nicht mal so wichtig,
aber ich will keine zwei unterschiedlichen Mechanismen haben, um
zwischen den gleichen zwei ISRs und main() zu kommunizieren.)
Aus Neugier habe ich es gerade noch ausprobiert, aber
__attribute__((_packed_)) ändert nichts am obenstehenden Kuriosum.
Sollte mich die "verpasste Gelegenheit zur Optimierung" dennoch mal
stören, reiche ich einfach die 16-Bit-Variable durch und lagere die
"Dekodierung" in ein Bitfeld in eine Static-Inline-Funktion aus. Wenn
das Bitfeld dann ausschließlich in der Funktion verwendet wird, in der
es auch erzeugt wird, wird es wegoptimiert und es bleiben die erwarteten
Operationen zurück.
Walter T. schrieb:> Rolf M. schrieb:>> Warum nimmst du nicht>> einfach einen Cast oder memcpy?>> Die ausführliche Schreibweise oben ist portabel.
Man könnte entsprechende static_asserts einbauen, um zu prüfen, dass die
Bitpositionen alle richtig sind. Dann wäre auch der Fehler, den du
gemacht hast, gleich aufgefallen ;-)
> So Optimierungs-Verzweifelt, dass ich implementationsabhängigen Code> einbauen will, bin ich an dieser Stelle nicht. Es ist ja eher ein> Kuriosum als ein echtes Problem.
Ok, akzeptiert.
Walter T. schrieb:> Hier will ich mal Eingänge nicht global haben, sondern> definiert per Funktionsaufrufen übergeben. (Im Hintergrund stellt dann> eine Doppelpufferstrategie sicher, dass die Zustände jeweils konsistent> sind.)
Man kann es mit der Kapselung aber auch übertreiben.
Bei nur 16 Eingängen sehe ich keinerlei Gefahr, den Überblick zu
verlieren, wenn sie global sind. Im Gegenteil, bei einer SPS sind ja
auch alle Pins global.
Walter T. schrieb:> in der Optimierungsstufe -O1
Also nur zur Sicherheit: das ist die höchste Optimierungsstufe und weder
Out noch Out.Input noch irgendwas anderes in Out sind irgendwie
volatile.
Ich mag generell keine Bitfelder und setze sie auch nicht direkt ein, da
sie nicht portabel sind. Auch kann man nicht leicht erkennen, welches
Bit welche Nummer hat. Man muß es im Bitfeld umständlich abzählen und
die Order kennen.
Es können sich also leicht Fehler einschleichen.
Das Macro in der sbit.h ist die Ausnahme und nur für den AVR-GCC
geprüft.
Mit der Benutzung des Macros ist dann wieder die Bitnummer sicher zu
erkennen, da direkt in der Definition der Bitvariablen angegeben.
Professionelle Header, z.B. für den LPC4357 im IAR benutzen immer nur
Bitnummern oder Masken, aber keine Bitfelder.
Peter D. schrieb:> Man kann es mit der Kapselung aber auch übertreiben.> Bei nur 16 Eingängen sehe ich keinerlei Gefahr, den Überblick zu> verlieren, wenn sie global sind.
Das ist eine Frage der Blickrichtung. Aus der Blickrichtung "da will
jemand Eingänge behandeln" muss sich die Folgerung, dass hier etwas
Einfaches sehr umständlich gemacht wird, aufdrängen. Und diese
Blickrichtung ergibt sich aus dem Threadverlauf zwangsläufig.
Jetzt eine andere Blickrichtung: Ich habe ein gutes Dutzend Variablen,
davon einige 64 Bit breit, die konsistent zwischen drei "Tasks"
ausgetauscht werden müssen. Dafür ist ein Mechanismus implementiert.
Jetzt habe ich 1x16 Bit, bei denen das nicht erforderlich ist. Da lohnt
es sich nicht, einen Extra-Mechanismus zu schreiben. Die werden einfach
dazugepackt.
A. S. schrieb:> Also nur zur Sicherheit
Es ist nur -O1, der Lebenszyklus von Out sieht so aus:
1
voidSlowTask(void)
2
{
3
staticSlowTaskOutput_tOut;
4
// Dem struct Out werden jede Menge Einzelwerte zugewiesen und nie gelesen
5
if(blabla)
6
SlowTaskOutputBuffer[1]=Out;
7
else
8
SlowTaskOutputBuffer[2]=Out;
9
}
Out ist static, weil nicht in jedem Aufruf der Funktion alles
geschrieben werden muss. SlowTaskOutputBuffer[] ist natürlich volatile,
aber wird ja nur per Wert überschrieben. input ist eine lokale Variable.
Ich sehe nichts, was eine Optimierung verhindern würde.
Peter D. schrieb:> Ich mag generell keine Bitfelder und setze sie auch nicht direkt ein
Ich mag sie generell auch nicht und erst Recht nicht, um Register zu
beschreiben. Aber hier schien einer der wenigen Fälle, wo sie passen und
keine Falle beinhalten. (Wenn man von der "Falle" absieht, dass man im
Gegensatz zu bool aufpassen muss, sie mit 1 oder 0 zu beschreiben und
nicht blos mit logischen Ausdrücken.)
Walter T. schrieb:> Aber hier schien einer der wenigen> Fällen, wo sie passen und keine Falle beinhalten.
Bezeichnend ist aber, daß Du erst einen falschen Code gepostet hast und
es Dir nicht aufgefallen ist. Erst im Assemblerlisting hat man sich
gewundert.
Die Falle ist also real und das Meiden von Bitfeldern ist völlig
berechtigt.
Walter T. schrieb:> Es ist nur -O1
Dann nimm doch spaßeshalber Mal die höchste. Und uint16_t als Datentyp.
Und fang damit an, alle Bits in einer Dummy-Funktion auf 1 zu setzen.
A. S. schrieb:> Dann nimm doch spaßeshalber Mal die höchste. Und uint16_t als Datentyp.
Mit -O3 wird die Funktionalität innerhalb der Funktion im
Assembler-Listing etwas auseinandergerissen, dass es zu unübersichtlich
ist, um hier gepostet zu werden. (In der Funktion passiert ja noch
mehr.) Es wird aber immer noch jedes Bit einzeln "kopiert".
Peter D. schrieb:> Bezeichnend ist aber, daß Du erst einen falschen Code gepostet hast und> es Dir nicht aufgefallen ist.
Hättest Du "falschen" in Anführungszeichen gesetzt, stimmte ich Dir zu.
Der Code war "falsch", aber nicht falsch. Er hat ja für jeden denkbaren
Wert das exakt erwartete Ergebnis erzeugt, das auch im weiteren
Programmablauf passend weiterverarbeitet wurde. Nur hat ihn der Compiler
nicht optimiert. Aber das macht er mit dem "richtigen" Code ja auch
nicht.
Walter T. schrieb:> ich habe eine 16-Bit-Variable und ein 16 Bit breites Bitfeld. Die> Reihenfolge im Bitfeld entspricht der Bit-Reihenfolge unter> Berücksichtigung der Endianness.
Bedenke mal, daß die ARM-Cortex Architektur keine Bitoperationen bzw.
Bitadressierungen kennt und in C ebenfalls kein Boolean Typ existiert.
Mit einer Programmiersprache, die sowas nur über Konventionen
realisieren kann (0 ist false, 1 ist true) zusammen mit einer
Zielarchitektur, die einzelne Bits nicht kennt sondern lediglich bei
einigen Chiptypen für einige Hardware-Bits (zumeist Portzustände)
passende Bitfelder (als char oder bis zu long) vorhält, ist das Erwarten
von ausgeknufften Optimierungen eine zu kühne Erwartung.
Andere Architekturen wie z.B. die PIC16 kennen Bitoperationen,
allerdings nur unter der einschränkenden Bedingung, daß sie dazu noch
die Adresse des Bytes benötigen, welches das betr. Bit enthält.
Aber bei Arhitekturen wie Arm oder Mips würde ich genauso wie Peter um
Bitfelder einen großen Bogen machen.
W.S.
Ich finde das Vorgehen des Compilers OK.
Es kann doch auch durchaus sein, dass die zeitliche Reihenfolge wichtig
ist.
Es könnten IOs dranhängen, die zeitlich nacheinander geschaltet werden
müssen. Woher sollte der Compiler wissen, dass das auch parallel gehen
kann?
Wenn du eine gleichzeitige Zuweisung haben möchtest, dann brauchst du
ein FPGA. Da ist die Reihenfolge der Zuweisungen egal, es wird immer mit
dem nächsten Takt alles gleichzeitig gesetzt.
W.S. schrieb:> Bedenke mal, daß die ARM-Cortex Architektur keine Bitoperationen bzw.> Bitadressierungen kennt
Bitfeld könne die Cortex M0 nicht, die mit Thumb2 aber schon. Der Code
vom TE ist Cortex.
Einzelbitadressierung per Bitbanding gibts auch im RAM, wieder ausser M0
(ohne +).
PittyJ schrieb:> Woher sollte der Compiler wissen, dass das auch parallel gehen> kann?
Er nimnmt das standardmäßig an. Alles, wo das NICHT der Fall ist, muss
markiert werden. (Stichwort -> volatile)
W.S. schrieb:> Bedenke mal, daß die ARM-Cortex Architektur keine Bitoperationen bzw.> Bitadressierungen kennt und in C ebenfalls kein Boolean Typ existiert.
Die Logik verstehe ich nicht. Kennte der Befehlssatz Bitoperationen,
könnte der Compiler sie in einer Schleife einsetzen (und würde das
Optimierungspotenzial evtl. übersehen, solange er kein Loop-Unrollig
macht). So muss er für jedes Bit beim lesen eine Maske berechnen und
beim Schreiben die selbe Maske berechnen und das Ganze achtmal für
jeweils die gleichen zwei Adressen. Da hätte ich eher erwartet, dass da
ein Penalty-Faktor draufliegt, der eine Optimierung erst anstößt.
Peter D. schrieb:> Ich mag generell keine Bitfelder und setze sie auch nicht direkt ein, da> sie nicht portabel sind.
Bitfelder sind durchaus portabel, wenn man sie so einsetzt, wie sie
gedacht sind, also nicht, um eine bestimmte Schnittstelle umzusetzen,
sondern lediglich um Platz zu sparen. Und wenn man das im Hinterkopf
behält, braucht man sie sowieso nur sehr selten.
Walter T. schrieb:> (Wenn man von der "Falle" absieht, dass man im> Gegensatz zu bool aufpassen muss, sie mit 1 oder 0 zu beschreiben und> nicht blos mit logischen Ausdrücken.)
Warum? Übrigens: Ein Bitfeld darf auch vom Typ bool sein.
W.S. schrieb:> Bedenke mal, daß die ARM-Cortex Architektur keine Bitoperationen bzw.> Bitadressierungen kennt und in C ebenfalls kein Boolean Typ existiert.
Hrmpf, warum musst du nur immer so einen Unsinn über C schreiben. Ein
boolescher Typ existiert in Standard-C seit 22 Jahren. Wie das
allerdings bei dieser Optimierung helfen soll, ist unklar.
PittyJ schrieb:> Ich finde das Vorgehen des Compilers OK.> Es kann doch auch durchaus sein, dass die zeitliche Reihenfolge wichtig> ist.
Davon muss der Compiler nur ausgehen, wenn eine der Variablen volatile
ist.
> Es könnten IOs dranhängen, die zeitlich nacheinander geschaltet werden> müssen. Woher sollte der Compiler wissen, dass das auch parallel gehen> kann?
Genau deshalb gibt es volatile - damit der Compiler weiß, dass jeder
Zugriff auch einzeln durchgeführt werden muss und nicht mit anderen
zusammengeführt oder in der Reihenfolge umsortiert werden darf.
Rolf M. schrieb:> Warum?
Weil ansonsten ein ungültiger Wert drin steht.
Rolf M. schrieb:> Übrigens: Ein Bitfeld darf auch vom Typ bool sein.
Das wusste ich auch nicht.
Und jetzt kommt der interessante Teil: Wenn alle Felder außer "reserved"
bool sind, werden im Assembler-Listing für die einzelnen Felder
unterschiedlich viele (2, 3 oder 4) Assembler-Instruktionen fällig. WTF?
Walter T. schrieb:> Und jetzt kommt der interessante Teil: Wenn alle Felder außer "reserved"> bool sind, werden im Assembler-Listing für die einzelnen Felder> unterschiedlich viele (2, 3 oder 4) Assembler-Instruktionen fällig. WTF?
Bei SWITCH_KEY0 ist das Bit schon an Position 0, weshalb UBFX entfallen
kann. Alle anderen bestehen durchweg aus UBFX und BFI. Bei den
gelegentlichen load/store Befehle solltest du einrechnen, dass BFI
keinen Speicher adressiert, sondern Register. Das Bitfeld wird ausserdem
als zwei Bytes verarbeitet, nicht als ein Halbwort.
Die !! vor dem logischen Ausdruck können jetzt entfallen, weil der
Compiler beim Bool-Feld den erlaubten Wertebereich kennt. Der Rest ist
gleich geblieben.
Aber stimmt: Er werden max. drei Befehle pro Bit. Der vierte kommt erst
beim Byte-Wechsel.
W.S. schrieb:> und in C ebenfalls kein Boolean Typ existiert.
Du solltest dein C-Wissen mal aktualisieren. <stdbool.h> gibt's seit
mehr als 20 Jahren, auch den Typ _Bool ("bool" konnte man ohne diesen
Header nicht konfliktfrei zum Standard hinzufügen, da legitime
Applikationen den Namen zuvor selbst definieren durften).
Walter T. schrieb:> Die !! vor dem logischen Ausdruck können jetzt entfallen, weil der> Compiler beim Bool-Feld den erlaubten Wertebereich kennt. Der Rest ist> gleich geblieben.
In der !! Version kommen die AND Befehle nicht vor - auf diese Version
hatte ich mich bezogen.
Gerade bei Bitfeldern sollte man nicht jede maximal mögliche Optimierung
erwarten. Nicht alles, was man machten könnte, betrachten die
Compilerbauer als wirklich wichtig. Weshalb dann schon mal ein nicht
optimales Schema genutzt wird.
(prx) A. K. schrieb:> In der !! Version kommen die AND Befehle nicht vor - auf diese Version> hatte ich mich bezogen.
Das liegt am Bool-Datentyp fürs Feld. Ist das Feld Bool, kommt mit oder
ohne !! das gleiche Assembly-Listing heraus (sieht man mal von den
Kommentaren ab).
(prx) A. K. schrieb:> Weshalb dann schon mal ein nicht> optimales Schema genutzt wird.
Ich merke es. Naja, was solls. Wenn anfängt zu jucken, mache ich das,
was ich oben geschrieben habe:
Walter T. schrieb:> reiche ich einfach die 16-Bit-Variable durch und lagere die> "Dekodierung" in ein Bitfeld in eine Static-Inline-Funktion aus.
Oder wahrscheinlich noch eher mit einem boolhaltigen struct als
Rückgabewert.
Mein Fazit: Der Compiler lässt sich selbst mit einem dicken Knüppel
nicht dazu überreden zu sehen, dass hier einfach ein Halbwort kopiert
werden will.
Aber auch: Für logische Oparation merkt er zumindest, dass er alles
wegoptimieren kann und von dem Struct als Zwischenergebnis bleibt auch
bei -O1 nichts mehr übrig. Damit ist das ein brauchbarer Workaround.
Rolf M. schrieb:> Hrmpf, warum musst du nur immer so einen Unsinn über C schreiben. Ein> boolescher Typ existiert in Standard-C seit 22 Jahren.
Das ist allerdings ein verkappter char und kein echter und
eigenständiger Datentyp.
Und da alle Arm/Cortex Architekturen im Grunde Load-Modify-Store
Architekturen sind, gibt es auch von der Architektur her keine
Bitoperationen auf diesen Plattformen, es ist immer das
Laden-Ändern-Speichern angesagt. Ich habe nicht ohne Grund die PIC16
angeführt, wo Befehle wie BTFSS, BTFSC, BSC und BSS existieren.
Nun ja, auf Maschinen mit einem RAM, der im Prinzip bis zu 4 GB groß
werden könnte, sind Befehle auf Einzelbits bzw. Boolean-Typen mit
tatsächlich nur einem Bit eher etwas Ungewöhnliches.
Jörg W. schrieb:> Du solltest dein C-Wissen mal aktualisieren. <stdbool.h> gibt's seit> mehr als 20 Jahren, auch den Typ _Bool...
Das alles kenne ich, aber es sind alles nur Header und am Schluß ist es
ein char, der 0 oder nicht 0 sein darf. Du hast ja explizit stdbool.h
angeführt, also wache du mal lieber auf und nimm die Tomaten von den
Augen.
Wir hatten die Datentypen-Diskussion ja vor Zeiten schon mal. Damals
ging es allerdings mehr um die Integer-Typen, wo jemand behauptete, daß
mit der passenden Header-Datei auch sowas wie uint16_t und Konsorten
existieren. In Wahrheit ist das alles nur ein Thema der Textersetzung,
sprich des Preprozessors und hat mit C nix wirklich zu tun. Wenn ich
Lust drauf hätte, könnte ich auch den Datentyp "ottokar" mittels eine
weiteren .h kreieren. Aber wozu?
W.S.
W.S. schrieb:> Wenn ich> Lust drauf hätte, könnte ich auch den Datentyp "ottokar" mittels eine> weiteren .h kreieren.
Mir wird der Zusammenhang mit den Bitfeldern nicht klar.
Walter T. schrieb:> ich habe eine 16-Bit-Variable und ein 16 Bit breites Bitfeld.
Und da es prinzipiell nicht festgelegt ist, an welcher Stelle in deiner
Variablen welches Bit zu stehen hat, kannst du auch nicht erwarten, daß
du mit der Annahme, daß du dieses durch manuelles Auszählen erledigen
kannst, einen Erfolg haben wirst.
Und da wir hier offenbar von einem "größeren" µC reden, wäre es wohl
eher angebracht, auf Bitfelder zu verzichten und die ganze Sache anders
zu erledigen.
Eigentlich ärgern mich immer Beiträge, wo jemand irgend eine Spezialecke
von C ausnutzen will, weil sie auf dem Papier so schön aussieht - und
dann wird darüber geklagt, daß man dem Compiler das Leben schwer macht
oder einen unerwartet großen Haufen Maschinencode als Ergebnis kriegt,
wo auf dem Papier doch alles so schön aussah.
W.S.
Für mich ist jede zusätzliche Umkodierung auch eine zusätzliche
Fehlerquelle, daher lasse ich alles in der Order, wie es anliegt.
Das macht es auch einfacher, die Signale in der Hardware und in der
Software zu debuggen.
Ich kann dann auch leicht die Zuordnung an einer einzigen Stelle ändern,
z.B. wenn ein Pin anderweitig benötigt wird. Der eigentliche Code muß
dazu nicht angefaßt werden, sondern nur neu compiliert.
Hast Du mal -Os probiert, das sollte den kleinsten Code ergeben.
W.S. schrieb:> Jörg W. schrieb:>> Du solltest dein C-Wissen mal aktualisieren. <stdbool.h> gibt's seit>> mehr als 20 Jahren, auch den Typ _Bool...>> Das alles kenne ich,
Den Eindruck habe ich nicht.
> aber es sind alles nur Header und am Schluß ist es> ein char,
Nein, am Schluss ist es – wie Jörg schon schrieb – ein _Bool.
> der 0 oder nicht 0 sein darf.
Nein, nur 0 oder 1.
> Du hast ja explizit stdbool.h angeführt, also wache du mal lieber auf> und nimm die Tomaten von den Augen.
In stdbool.h wird (u.a.) bool als _Bool definiert, aber nicht – wie du
immer wieder fälschlicherweise behauptest – als char.
Walter T. schrieb:> Man sieht: Es wird jedes Bit einzeln geprüft und zugewiesen, und man> sieht auch, dass der Bit-Offset immer gleich ist. Ich hätte erwartet,> dass der Compiler daraus eine Maskierung und eine Zuweisung macht.>> Was hindert ihn daran?
Keine Ahnung.
Ich kann dir nur sagen, dass es mit auf dem PC mit
Microsoft cl als auch mit clang-cl einwandfrei funktioniert.
Der gcc mag das aber nicht.
Ich habe nur mit 2 Bits getestet:
1
#include<stdio.h>
2
#include<string.h>
3
4
structInputs_s{
5
unsignedintmpgKey:1;/**< Funktionstaste am Handrad gedrueckt */
Yalu X. schrieb:> Nein, nur 0 oder 1.
Und wenn die Initalisierung fehlt, auch irgendein anderer Wert. Fragt
nicht, woher ich das weiß.
udok schrieb:> memset(&Out, 0, sizeof(Out));
Das ist ein anderes Spiel. Das ist ja wirklich implementationsabhängig.
Walter T. schrieb:> udok schrieb:>> memset(&Out, 0, sizeof(Out));>> Das ist ein anderes Spiel. Das ist ja wirklich implementationsabhängig.
Du kannst die Variable natürlich anders initialisieren...
Warum soll das aber implementierungsabhängig sein?
http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1256.pdf
A b it-field is interpreted as a signed or unsigned integer
type consisting of the specified
number of bits.107) If the value 0 or 1 is stored into a
nonzero-width bit-field of type
_Bool, t he value of the bit-field shall compare equal to the value
stored.
Hmm... das ist der Standard strenger als wir. Offensichtlich muss doch
der Programmierer sicherstellen, dass in einem bool-Feld nur 0 oder 1
gespeichert wird.
udok schrieb:> Warum soll das aber implementierungsabhängig sein?
The order of allocation of bit-fields within a unit (high-order to
low-order or low-order to high-order) is implementation-defined.
The alignment of the addressable storage unit is unspecified
Mache ich das Feld "reserved" zu einem anonymen Feld (unnamed field, das
kannte ich bis jetzt auch noch nicht), wird es komplett wild und die
Codegröße verdoppelt sich noch einmal.
W.S. schrieb:> Rolf M. schrieb:>> Hrmpf, warum musst du nur immer so einen Unsinn über C schreiben. Ein>> boolescher Typ existiert in Standard-C seit 22 Jahren.>> Das ist allerdings ein verkappter char und kein echter und> eigenständiger Datentyp.
Nein, das ist ein eigener Datentyp. Wenn ich bool b = 3 schreibe, steht
in b nachher keine 3 drin, da das kein gültiger Wert für bool ist.
> Nun ja, auf Maschinen mit einem RAM, der im Prinzip bis zu 4 GB groß> werden könnte, sind Befehle auf Einzelbits bzw. Boolean-Typen mit> tatsächlich nur einem Bit eher etwas Ungewöhnliches.
Nun sind aber ARMe mit nur wenigen kB RAM auch nichts ungewöhnliches.
> Das alles kenne ich, aber es sind alles nur Header und am Schluß ist es> ein char, der 0 oder nicht 0 sein darf. Du hast ja explizit stdbool.h> angeführt, also wache du mal lieber auf und nimm die Tomaten von den> Augen.
Davon stimmt auch nur die Hälfte. Ja, bool ist in einem
(Standard!)-Header definiert, aber mit char hat das nichts zu tun. Der
relevante Teil des zu meinem Compiler gehörenden stdbool.h:
1
#define bool _Bool
2
#define true 1
3
#define false 0
Und C schreibt vor, dass das so definiert sein muss. _Bool ist ein
eigenständiger Datentyp. Der Umweg über den Header wird nur gemacht,
weil viele Programme vorher ihr eigenes bool definiert haben. Damit die
nicht plötzlich alle ein Problem haben, weil bool jetzt ein
Schlüsselwort ist, wurde es stattdessen _Bool genannt und nur über einen
Header auf bool gemappt. So kann man bei einem vor-C99-Programm wählen,
ob man bool haben will oder nicht.
> Wir hatten die Datentypen-Diskussion ja vor Zeiten schon mal. Damals> ging es allerdings mehr um die Integer-Typen, wo jemand behauptete, daß> mit der passenden Header-Datei auch sowas wie uint16_t und Konsorten> existieren.
Das ist nicht nur eine Behauptung, sondern Fakt. Die "passenden
Header-Dateien" sind Teil des C-Standards. Jeder C99-konforme Compiler
muss die in korrekter Form mitbringen, sonst verstößt er gegen den
C-Standard. Kann man in dem oben schon verlinkten Dokument
http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1256.pdf in Kapitel
7.18 "Integer types <stdint.h>" nachlesen.
> In Wahrheit ist das alles nur ein Thema der Textersetzung,> sprich des Preprozessors und hat mit C nix wirklich zu tun.
Natürlich hat die C-Standardbibliothek was mit C zu tun. uint16_t ist
Teil davon, genau wie z.B. printf() oder malloc().
> Wenn ich Lust drauf hätte, könnte ich auch den Datentyp "ottokar" mittels> eine weiteren .h kreieren. Aber wozu?
Weiß ich nicht. Aber ich weiß auch nicht, was dein selbst definierter
Header mit der Standardbibliothek zu tun haben soll.
W.S. schrieb:> Das alles kenne ich, aber es sind alles nur Header und am Schluß ist es> ein char, der 0 oder nicht 0 sein darf.
Nein, ist es nicht. Im Gegensatz zu einem char, uint8_t etc. kümmert
sich der Compiler nämlich drum, dass es nur die Werte 0 und 1 annehmen
kann – auch wenn du 42 drauf zuweist.
1
$ cat foo.c
2
_Bool foo = 42;
3
$ cc -Os -S foo.c
4
$ cat foo.s
5
.text
6
.file "foo.c"
7
.type foo,@object # @foo
8
.data
9
.globl foo
10
foo:
11
.byte 1 # 0x1
12
.size foo, 1
13
14
.ident "FreeBSD clang version 10.0.1 (git@github.com:llvm/llvm-project.git llvmorg-10.0.1-0-gef32c611aa2)"
15
.section ".note.GNU-stack","",@progbits
16
.addrsig
> Du hast ja explizit stdbool.h> angeführt, also wache du mal lieber auf und nimm die Tomaten von den> Augen.
_Bool existiert auch ohne diesen Header, der Header kümmert sich nur
drum, bool, true und false noch mit einzubringen (die vor C99 / ohne
<stdbool.h> im application namespace liegen).
Sorry, du solltest vielleicht Standards auch mal lesen, bevor du dich
über sie äußerst.
udok schrieb:> Ok, habs.>> Der gcc will sowas sehen:>
1
>structInputs_sinput_normalize(unsignedinput)
2
>{
3
>structInputs_sOut;
4
>
5
>*(unsigned*)(&Out)=0;
6
>}
7
>
Das ist nicht erlaubt da es gegen die aliasing regeln verstößt. Man darf
Objekte nur durch Pointer mit dem Typen des Objekten modifizieren
(Ausnahme char).
Bitfields können für Register sehr praktisch sein, da der Compiler das
shiften und maskieren automatisch übernimmt. Dadurch fallen mehrere
potentiell Fehlerquellen weg (z.B. muss Wert vor Übergabe geschiftet
werden oder tut das die Funktion?)
Und mit C++20 und constexpr bit_cast kann man auch endlich das Layout
von Bitfields checken. Per #ifdef kann man dann auch die Reihenfolge
umdrehen für big endian. Damit dürften nur noch esoterische Fälle
überbleiben wo das nicht kompiliert.
Welcher C/C++ Standard? Ggf. ist die erwartete "Optimierung" hier
illegal, da der Compiler keine writes erfinden darf. Deine Erwartung ist
ja das alle Bits zusammen geschrieben werden. Das steht aber nicht im
Code. Wenn z.b. Bit 0 geschrieben wird, darf (ggf) keines der anderen
Bits angefasst werden. Das ist auch bei Vektortypen (SSE, AVX, NEON,
SVE) so und hat dort schon zu lustigen Bugs geführt.
Aber sofern nur eine Vermutung von mir.
bitfield schrieb:> udok schrieb:>> Ok, habs.>>>> Der gcc will sowas sehen:>>> struct Inputs_s input_normalize(unsigned input)>> {>> struct Inputs_s Out;>>>> *(unsigned*)(&Out) = 0;>> }>>>> Das ist nicht erlaubt da es gegen die aliasing regeln verstößt. Man darf> Objekte nur durch Pointer mit dem Typen des Objekten modifizieren> (Ausnahme char).
Ein memcpy würde aber gehen.
MaWin schrieb:> Hannes schrieb:>>> Das steht aber nicht im>> Code>> as-if-Regel.
Trifft hier nicht zu. Der Compiler darf keine Schreibzugriffe erfinden,
auch wenn sie keine Daten im Speicher verändern, da sonst Locks nicht
mehr funktionieren. MMn heißt das hier, die Bitzugriffe dürfen nicht
zusammengefasst werden. Das ist etwas anderes als wenn z.B. bei mehreren
aufeinander folgenden Schreibzugriffen auf Variablen, die ohne RMW
auskommen, nur der letzte Schreibzugriff ausgeführt wird.
Bitfelder decken sich hinsichtlich Konsistenz nicht wirklich gut mit den
Regeln für normale Daten, da Operationen auf einem bestimmten Bitfeld
auch auf die Nachbarbits wirken. Die werden zwar nicht verändert, aber
gelesen und später wieder zurückgeschrieben, ohne dass sich das im
Quelltext niederschlägt.
Es könnte sich vielleicht lohnen, in der Standards und den Kommentaren
dazu speziell danach zu graben.
(prx) A. K. schrieb:> Es könnte sich vielleicht lohnen, in der Standards und den Kommentaren> dazu speziell danach zu graben.
Der Standard hält sich in Bezug auf Bitfelder extrem kurz. Es gibt
keinen Anlaß dazu das so zu lesen, als ob sie irgendwie anders als
normale Struct-Felder zu sehen seien. Lediglich beim Padding gibt es
eine Sonderregelung.
Sagen die Kommentare etwas Anderes?
Walter T. schrieb:> Der Standard hält sich in Bezug auf Bitfelder extrem kurz. Es gibt> keinen Anlaß dazu das so zu lesen, als ob sie irgendwie anders als> normale Struct-Felder zu sehen seien.
Das ist ja das Problem. Konsequent betrachtet dürfte es Bitfelder
vielleicht überhaupt nicht geben. Denn ein Zugriff auf .driveStatusOk
betrifft auch .chargePumpOk.
> Sagen die Kommentare etwas Anderes?
Eigentlich hatte ich vor, diesmal andere Leute suchen zu lassen. ;-)
Walter T. schrieb:> Der Standard hält sich in Bezug auf Bitfelder extrem kurz. Es gibt> keinen Anlaß dazu das so zu lesen, als ob sie irgendwie anders als> normale Struct-Felder zu sehen seien. Lediglich beim Padding gibt es> eine Sonderregelung.
Lies noch mal nach im Standard. Da steht noch mehr...
>> Sagen die Kommentare etwas Anderes?
Ich heute auch keine Lust, für dich zu suchen.
(prx) A. K. schrieb:> Bitfelder decken sich hinsichtlich Konsistenz nicht wirklich gut mit den> Regeln für normale Daten, da Operationen auf einem bestimmten Bitfeld> auch auf die Nachbarbits wirken. Die werden zwar nicht verändert, aber> gelesen und später wieder zurückgeschrieben, ohne dass sich das im> Quelltext niederschlägt.>> Es könnte sich vielleicht lohnen, in der Standards und den Kommentaren> dazu speziell danach zu graben.
Wenn du Bitfelder als Teil eines normalen Structs im RAM verwendest,
dann funktioniert das eigentlich sehr gut.
Wenn hinter dem Struct HW Register liegen, dann kann es Probleme geben.
Wobei moderne Controller eh meist separate Read und Write Register
zum Lesen und Setzen einzelner Bits haben.
Die speziellen Bitbefehle, werden von Compilern meist ignoriert.
udok schrieb:> Die speziellen Bitbefehle, werden von Compilern meist ignoriert.
Siehe oben, ganz am Anfang vom Thread. Überwiegend Bitfeldbefehle.
Allerdings natürlich im Register, ist ja eine Load/Store Architektur.
Die Einzelbitbefehle der AVRs werden regelmässig verwendet.
> Wenn du Bitfelder als Teil eines normalen Structs im RAM verwendest,> dann funktioniert das eigentlich sehr gut.
Zur Illustration, wo man dabei landen kann:
https://lwn.net/Articles/478657/
(prx) A. K. schrieb:> Konsequent betrachtet dürfte es Bitfelder> vielleicht überhaupt nicht geben. Denn ein Zugriff auf .driveStatusOk> betrifft auch .chargePumpOk.
Und das ist okay nach der as-if-Regel. Hier ist nichts volatile. Jedes
Feld wird so beschrieben, wie es ausgelesen wird. Kein type-punning,
kein Alias. Was der Compiler daraus macht, ist ja so weit richtig. Nur
umständlich.
(prx) A. K. schrieb:> Es könnte sich vielleicht lohnen, in der Standards und den Kommentaren> dazu speziell danach zu graben.
Nö, das lohnt sich mMn überhaupt nicht wirklich. Stattdessen lohnt es
sich, seine Vorstellungen über die gewünschte Funktionalität anders zu
formulieren.
Eigentlich benötigt man für eine Boolean-Variable nur ein einziges Bit -
aber mir ist kein Compiler bekannt, der sowas tut. Stattdessen belegt
der Compiler für eine boolean-Variable zumindest die kleinste
Speichermenge, die auf der Zielmaschine adressierbar ist - und das ist
heutzutage eigentlich immer das Byte, im C Sprachgebrauch char genannt.
(über das Verquicken von Textzeichen mit Rechengrößen will ich hier
nicht nochmal diskutieren) und 7 der 8 Bit des besagten Bytes sind quasi
'Verschnitt', der salopp gesagt Abfall ist.
Von Microsoft kenne ich auch Dinge wie Longbool, wo auch nur true oder
false gespeichert werden sollen, aber insgesamt 32 Bit damit verbraucht
werden.
So - und nun will der TO die einzelnen Bits einer 16 Bit Variablen je
nach 16 verschiedenen boolean-Variablen setzen. So wie ich das sehe,
macht es der Compiler bereits so gut wie er das kann - aber dem TO
reicht das noch immer nicht. Nach meiner Ansicht ist das Erwarten von
noch kompakterem Maschinencode schlichtweg übertrieben.
Es ist wie ich bereits sagte: Man landet bei boolean zumindest auf einem
Byte (bzw. char) und nicht von selbst bei nur einem Bit und da gestaltet
sich das Einsortieren von boolean-Variablen bzw. Zuständen in die Bits
einer numerischen Variablen eben entsprechen umständlich - weil man die
einzelnen Bits nicht separat voeinander ansprechen kann.
Was auf dem Papier so schön einfach aussieht, ist realiter ein größeres
Problem, als man durch bloßes Draufschauen auf das Geschriebene meinen
mag.
Da helfen auch die Einlassungen von Jörg nix, denn irgendwelche Papiere
irgendwelcher Gremien können an den technischen Gegebenheiten kaun etwas
ändern. Also ist eher das Nachdenken über das, was man tun will nötig,
als das nochmalige Lesen der Standards.
W.S.
(prx) A. K. schrieb:>> Wenn du Bitfelder als Teil eines normalen Structs im RAM verwendest,>> dann funktioniert das eigentlich sehr gut.>> Zur Illustration, wo man dabei landen kann:> https://lwn.net/Articles/478657/
Armes Schwein, der den Fehler suchen musste.
Das macht aber nicht nur der Compiler so, sondern auch die CPU.
Jedesmal, wenn du 1 Byte liest, werden in Wirklichkeit 64 Bytes
gelesen, genauso beim schreiben (Cacheline).
Das sieht man auch bei den memcmp und memcpy Tests, die ich gemacht
habe.
Da ist es ziemlich egal, ob man 1 Byte oder 32 Bytes vergleicht.
Dauert alles gleich kurz.
"volatile" hilft dir da auch nicht, das ist nur für den Compiler da.
Da braucht es einen "lock" prefix für Multiprozessor Synchronisation.
Da wird dann eine HW Leitung gesetzt, damit der Wert im L3 Cache landet,
wo ihn auch die anderen Prozessoren sehen.
Wobei das auch nicht mehr ganz wahr ist, in aktuellen Prozessoren.
(prx) A. K. schrieb:>> Die speziellen Bitbefehle, werden von Compilern meist ignoriert.>> Siehe oben, ganz am Anfang vom Thread. Überwiegend Bitfeldbefehle.> Allerdings natürlich im Register, ist ja eine Load/Store Architektur.
Bezog mich auf die Intel Bitbefehle, die sehe ich fast nie.
Macht aber wahrscheinlich auch keinen Sinn, weil die & und | Befehle
da genauso schnell sind.
udok schrieb:> Bezog mich auf die Intel Bitbefehle, die sehe ich fast nie.
Weil langsamer. BT m,i sind bei Intel 2 Microops, TEST m.i nur einer.
Bei Intels Dekoderstruktur waren Befehle mit mehr als einem Microop seit
dem Pentium Pro deutlich benachteiligt. BTC/BTR/BTS sind teils noch
schlimmer dran.
Und weil seit jeher kaum jemand diese Befehle verwendete, werden sie von
den Prozessorbauern auch nicht optimal umgesetzt. Weshalb sie auch heute
keiner verwendet.
Hannes schrieb:> MaWin schrieb:>> Hannes schrieb:>>>>> Das steht aber nicht im>>> Code>>>> as-if-Regel.>> Trifft hier nicht zu.
Doch, natürlich trifft die zu. Der Compiler darf laut as-if-Regel alles
beliebig verändern, außer volatile-Zugriffen, File-I/O und Terminal-I/O.
Alles, aber wirklich alles andere ist durch die as-if-Regel quasi "zum
Abschuss freigegeben."
Hier mal in Standard-Sprech:
"The least requirements on a conforming implementation are:
— At sequence points, volatile objects are stable in the sense that
previous accesses are complete and subsequent accesses have not yet
occurred.
— At program termination, all data written into files shall be identical
to the result that execution of the program according to the abstract
semantics would have produced.
— The input and output dynamics of interactive devices shall take place
as specified in 7.19.3."
> Der Compiler darf keine Schreibzugriffe erfinden, auch wenn sie keine> Daten im Speicher verändern, da sonst Locks nicht mehr funktionieren.
Es geht doch gar nicht um das Erfinden von Schreibzugriffen, sondern
darum, welche zu entfernen. Statt für jedes Bit separat auf die Variable
zuzugreifen, soll der Zugriff nur einmal erfolgen.