Kontext
Wir portieren C-Code, der ursprünglich mit einem 8-Bit-C-Compiler für den PIC-Mikrocontroller kompiliert wurde. Eine gebräuchliche Redewendung, die verwendet wurde, um zu verhindern, dass vorzeichenlose globale Variablen (z. B. Fehlerzähler) auf Null zurückgesetzt werden, lautet wie folgt:
if(~counter) counter++;
Der bitweise Operator invertiert hier alle Bits und die Anweisung ist nur wahr, wenn sie counter
kleiner als der Maximalwert ist. Wichtig ist, dass dies unabhängig von der variablen Größe funktioniert.
Problem
Wir zielen jetzt auf einen 32-Bit-ARM-Prozessor mit GCC. Wir haben festgestellt, dass derselbe Code unterschiedliche Ergebnisse liefert. Soweit wir das beurteilen können, scheint die bitweise Komplementoperation einen Wert zurückzugeben, der eine andere Größe hat als wir erwarten würden. Um dies zu reproduzieren, kompilieren wir in GCC:
uint8_t i = 0;
int sz;
sz = sizeof(i);
printf("Size of variable: %d\n", sz); // Size of variable: 1
sz = sizeof(~i);
printf("Size of result: %d\n", sz); // Size of result: 4
In der ersten Ausgabezeile erhalten wir das, was wir erwarten würden: i
1 Byte. Das bitweise Komplement von i
beträgt jedoch tatsächlich vier Bytes, was ein Problem verursacht, da Vergleiche mit diesen jetzt nicht die erwarteten Ergebnisse ergeben. Zum Beispiel, wenn Sie Folgendes tun (wo i
ist eine ordnungsgemäß initialisierte uint8_t
):
if(~i) i++;
Wir werden sehen, wie i
von 0xFF zurück zu 0x00 "gewickelt" wird. Dieses Verhalten unterscheidet sich in GCC von dem, als es früher wie im vorherigen Compiler und 8-Bit-PIC-Mikrocontroller vorgesehen funktioniert hat.
Wir sind uns bewusst, dass wir dies durch Casting wie folgt lösen können:
if((uint8_t)~i) i++;
Oder von
if(i < 0xFF) i++;
In beiden Problemumgehungen muss jedoch die Größe der Variablen bekannt sein und ist für den Softwareentwickler fehleranfällig. Diese Arten von Obergrenzenprüfungen finden in der gesamten Codebasis statt. Es gibt mehrere Größen von Variablen (zB., uint16_t
Und unsigned char
usw.) verwendet wird und dies in einer ansonsten arbeitet Code - Basis ist nicht etwas , das wir freuen uns auf.
Frage
Ist unser Verständnis des Problems korrekt und gibt es Optionen zur Lösung dieses Problems, die nicht einen erneuten Besuch in jedem Fall erfordern, in dem wir diese Redewendung verwendet haben? Ist unsere Annahme richtig, dass eine Operation wie das bitweise Komplement ein Ergebnis zurückgeben sollte, das dieselbe Größe wie der Operand hat? Es scheint, als würde dies je nach Prozessorarchitektur brechen. Ich habe das Gefühl, ich nehme verrückte Pillen und C sollte etwas tragbarer sein. Auch hier könnte unser Verständnis falsch sein.
Oberflächlich betrachtet scheint dies kein großes Problem zu sein, aber diese zuvor funktionierende Redewendung wird an Hunderten von Standorten verwendet, und wir sind gespannt darauf, dies zu verstehen, bevor wir mit teuren Änderungen fortfahren.
Hinweis: Hier gibt es eine scheinbar ähnliche, aber nicht exakte doppelte Frage: Die bitweise Operation auf char liefert ein 32-Bit-Ergebnis
Ich habe nicht gesehen, dass der eigentliche Kern des dort diskutierten Problems darin besteht, dass die Ergebnisgröße eines bitweisen Komplements anders ist als die, die an den Operator übergeben wird.
quelle
Antworten:
Was Sie sehen, ist das Ergebnis ganzzahliger Aktionen . In den meisten Fällen, in denen ein ganzzahliger Wert in einem Ausdruck verwendet wird, wird der Werttyp kleiner als
int
der Wert heraufgestuftint
. Dies ist in Abschnitt 6.3.1.1p2 des C-Standards dokumentiert :Wenn eine Variable den Typ
uint8_t
und den Wert 255 hat, wird sie durch Verwendung eines anderen Operators als einer Umwandlung oder Zuweisung zuerst in einen Typint
mit dem Wert 255 konvertiert, bevor die Operation ausgeführt wird. Deshalb erhaltensizeof(~i)
Sie 4 statt 1.In Abschnitt 6.5.3.3 wird beschrieben, dass für den
~
Betreiber ganzzahlige Werbeaktionen gelten :int
Wenn Sie also ein 32-Bit annehmen , wenncounter
es den 8-Bit-Wert0xff
hat, wird es in den 32-Bit-Wert konvertiert0x000000ff
, und wenn Sie es anwenden~
, erhalten Sie0xffffff00
.Der wahrscheinlich einfachste Weg, dies zu handhaben, besteht darin, zu überprüfen, ob der Wert nach dem Inkrementieren 0 ist, und ihn gegebenenfalls zu dekrementieren, ohne den Typ kennen zu müssen.
Der Umlauf von Ganzzahlen ohne Vorzeichen funktioniert in beide Richtungen. Wenn Sie also einen Wert von 0 dekrementieren, erhalten Sie den größten positiven Wert.
quelle
if (!++counter) --counter;
Für manche Programmierer ist dies möglicherweise weniger seltsam als die Verwendung des Kommaoperators.++counter; counter -= !counter;
.increment_unsigned_without_wraparound
oderincrement_with_saturation
. Persönlich würde ich eine generische Tri-Operanden-clamp
Funktion verwenden.in der Größe von (i); Sie fordern die Größe der Variablen i an , also 1
in der Größe von (~ i); Sie fordern die Größe des Ausdruckstyps an, der in Ihrem Fall ein int ist. 4
Benutzen
zu wissen, ob ich 255 nicht wert bin (in deinem Fall mit einem uint8_t) ist nicht sehr lesbar, tu es einfach
und Sie haben einen tragbaren und lesbaren Code
So verwalten Sie jede Größe von nicht signierten:
Der Ausdruck ist konstant und wird zur Kompilierungszeit berechnet.
#include <limit.h> für CHAR_BIT und #include <stdint.h> für uintmax_t
quelle
!= 255
ist daher unzureichend.unsigned
Objekte undefiniert , da Verschiebungen der gesamten Objektbreite nicht durch den C-Standard definiert sind, sondern mit festgelegt werden können(2u << sizeof(i)*CHAR_BIT-1) - 1
.((uintmax_t) 2 << sizeof(i)*CHAR_BIT-1) - 1
.Hier sind verschiedene Optionen für die Implementierung von "Add 1 to
x
but clamp at the maximal darstellbaren Wert", vorausgesetzt, esx
handelt sich um einen vorzeichenlosen Integer-Typ:Fügen Sie genau dann einen hinzu, wenn er
x
kleiner als der in seinem Typ darstellbare Maximalwert ist:Siehe den folgenden Punkt für die Definition von
Maximum
. Diese Methode hat gute Chancen, von einem Compiler für effiziente Anweisungen wie Vergleichen, irgendeine Form von bedingter Menge oder Verschiebung und Hinzufügen optimiert zu werden.Vergleichen Sie mit dem größten Wert des Typs:
(Dies berechnet 2 N , wobei N die Anzahl der Bits ist
x
, indem 2 um N - 1 Bits verschoben werden . Wir tun dies, anstatt 1 N Bits zu verschieben, da eine Verschiebung um die Anzahl der Bits in einem Typ nicht durch C definiert ist Standard. DasCHAR_BIT
Makro ist einigen vielleicht unbekannt. Es ist die Anzahl der Bits in einem Byte, ebensosizeof x * CHAR_BIT
wie die Anzahl der Bits in der Art vonx
.)Dies kann aus Gründen der Ästhetik und Klarheit wie gewünscht in ein Makro eingeschlossen werden:
Inkrementieren
x
und korrigieren Sie, wenn es auf Null gesetzt wird, indem Sie Folgendes verwendenif
:Inkrementieren
x
und korrigieren Sie, wenn es mit einem Ausdruck auf Null gesetzt wird:Dies ist nominell verzweigungslos (manchmal vorteilhaft für die Leistung), aber ein Compiler kann es genauso wie oben implementieren und bei Bedarf eine Verzweigung verwenden, möglicherweise jedoch mit bedingungslosen Anweisungen, wenn die Zielarchitektur über geeignete Anweisungen verfügt.
Eine verzweigungslose Option unter Verwendung des obigen Makros ist:
Wenn
x
das Maximum seines Typs ist, wird dies ausgewertetx += 1-1
. Ansonsten ist esx += 1-0
. Bei vielen Architekturen ist die Teilung jedoch etwas langsam. Ein Compiler kann dies abhängig vom Compiler und der Zielarchitektur für Anweisungen ohne Unterteilung optimieren.quelle
-Wshift-op-parentheses
. Die gute Nachricht ist, dass ein optimierender Compiler hier keine Division generiert, sodass Sie sich keine Sorgen machen müssen, dass diese langsam ist.sizeof x
kann nicht in einer C-Funktion implementiert werden, dax
dies ein Parameter (oder ein anderer Ausdruck) mit einem festen Typ sein müsste. Es konnte nicht die Größe des vom Aufrufer verwendeten Argumenttyps erzeugen. Ein Makro kann.Vor stdint.h können die Variablengrößen von Compiler zu Compiler variieren, und die tatsächlichen Variablentypen in C sind immer noch int, long usw. und werden vom Compilerautor immer noch hinsichtlich ihrer Größe definiert. Weder einige Standard- noch Ziel-spezifische Annahmen. Die Autoren müssen dann stdint.h erstellen, um die beiden Welten abzubilden. Dies ist der Zweck von stdint.h, um die uint_this auf int, long, short abzubilden.
Wenn Sie Code von einem anderen Compiler portieren und dieser char, short, int, long verwendet, müssen Sie jeden Typ durchgehen und den Port selbst ausführen. Daran führt kein Weg vorbei. Und entweder haben Sie die richtige Größe für die Variable, die Deklaration ändert sich, aber der Code wie geschrieben funktioniert ....
oder ... die Maske oder Typografie direkt bereitstellen
Wenn Sie möchten, dass dieser Code funktioniert, müssen Sie ihn letztendlich auf die neue Plattform portieren. Ihre Wahl, wie. Ja, Sie müssen die Zeit damit verbringen, jeden Fall zu treffen und es richtig zu machen, sonst werden Sie immer wieder auf diesen Code zurückkommen, der noch teurer ist.
Wenn Sie die Variablentypen im Code vor dem Portieren isolieren und welche Größe die Variablentypen haben, isolieren Sie die Variablen, die dies tun (sollte leicht zu erfassen sein), und ändern Sie ihre Deklarationen mithilfe von stdint.h-Definitionen, die sich hoffentlich in Zukunft nicht ändern werden. und Sie wären überrascht, aber manchmal werden die falschen Header verwendet, also checken Sie sogar ein, damit Sie nachts besser schlafen können
Und während diese Art der Codierung funktioniert (if (~ counter) counter ++;), ist es für Portabilitätswünsche jetzt und in Zukunft am besten, eine Maske zu verwenden, um die Größe spezifisch zu begrenzen (und sich nicht auf die Deklaration zu verlassen) Der Code wird an erster Stelle geschrieben oder beenden Sie einfach den Port, und dann müssen Sie ihn an einem anderen Tag nicht erneut portieren. Oder um den Code besser lesbar zu machen, machen Sie das, wenn x <0xFF then oder x! = 0xFF oder ähnliches, dann kann der Compiler ihn in den gleichen Code optimieren, den er für jede dieser Lösungen verwenden würde, macht ihn nur lesbarer und weniger riskant ...
Dies hängt davon ab, wie wichtig das Produkt ist oder wie oft Sie Patches / Updates versenden oder einen LKW rollen oder zum Labor gehen möchten, um festzustellen, ob Sie versuchen, eine schnelle Lösung zu finden oder nur die betroffenen Codezeilen zu berühren. Wenn es nur hundert oder wenige sind, ist das kein so großer Hafen.
quelle
C 2011 Online-Entwurf
Das Problem ist, dass der Operand von
~
wird,int
bevor der Operator angewendet wird.Leider glaube ich nicht, dass es einen einfachen Ausweg gibt. Schreiben
wird nicht helfen, da dort auch Werbeaktionen gelten. Das einzige , was ich vorschlagen kann , ist einige symbolische Konstanten für die maximal zu schaffen Wert Sie das Objekt möchten , dass darstellen und Prüfung gegen:
quelle
-1
nicht benötigt wird, da dies dazu führen würde, dass sich der Zähler bei 254 (0xFE) einstellt. In jedem Fall ist dieser Ansatz, wie in meiner Frage erwähnt, aufgrund der unterschiedlichen Variablengrößen in der Codebasis, die an dieser Redewendung teilnehmen, nicht ideal.