Die bitweise Operation führt zu einer unerwarteten Variablengröße

24

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 counterkleiner 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: i1 Byte. Das bitweise Komplement von ibeträ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 iist eine ordnungsgemäß initialisierte uint8_t):

if(~i) i++;

Wir werden sehen, wie ivon 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_tUnd unsigned charusw.) 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.

Charlie Salts
quelle
14
"Ist unsere Annahme richtig, dass eine Operation wie das bitweise Komplement ein Ergebnis zurückgeben sollte, das dieselbe Größe wie der Operand hat?" Nein, dies ist nicht korrekt. Es gelten ganzzahlige Aktionen.
Thomas Jager
2
Obwohl dies sicherlich relevant ist, bin ich nicht davon überzeugt, dass es sich um Duplikate dieser speziellen Frage handelt, da sie keine Lösung für das Problem bieten.
Cody Grey
3
Ich habe das Gefühl, ich nehme verrückte Pillen und C sollte etwas tragbarer sein. Wenn Sie für 8-Bit-Typen keine Ganzzahl-Promotions erhalten haben, war Ihr Compiler nicht mit dem C-Standard kompatibel. In diesem Fall sollten Sie meiner Meinung nach alle Berechnungen durchgehen , um sie zu überprüfen und gegebenenfalls zu korrigieren.
user694733
1
Bin ich der einzige, der sich fragt, welche Logik, abgesehen von wirklich unwichtigen Zählern, dazu führen kann, dass "erhöht wird, wenn genügend Platz vorhanden ist, sonst vergessen"? Wenn Sie Code portieren, können Sie int (4 Byte) anstelle von uint_8 verwenden? Das würde Ihr Problem in vielen Fällen verhindern.
Puck
1
@puck Sie haben Recht, wir könnten es auf 4 Bytes ändern, aber es würde die Kompatibilität bei der Kommunikation mit vorhandenen Systemen beeinträchtigen. Die Absicht ist , zu wissen , wenn es irgendwelche Fehler und so ein 1-Byte - Zähler war ursprünglich ausreichend, und bleibt so.
Charlie Salts

Antworten:

26

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 intder Wert heraufgestuft int. Dies ist in Abschnitt 6.3.1.1p2 des C-Standards dokumentiert :

Das Folgende kann in einem Ausdruck verwendet werden, wo immer ein intoder unsigned intverwendet werden kann

  • Ein Objekt oder Ausdruck mit einem ganzzahligen Typ (außer intoder unsigned int), dessen ganzzahliger Konvertierungsrang kleiner oder gleich dem Rang von intund ist unsigned int.
  • Ein Bitfeld vom Typ _Bool, int ,signiert int , orunsigned int`.

Wenn a intalle Werte des ursprünglichen Typs darstellen kann (wie durch die Breite für ein Bitfeld eingeschränkt), wird der Wert in a konvertiert int. Andernfalls wird es in ein konvertiert unsigned int. Diese werden als Integer-Promotions bezeichnet . Alle anderen Typen bleiben durch die ganzzahligen Aktionen unverändert.

Wenn eine Variable den Typ uint8_tund den Wert 255 hat, wird sie durch Verwendung eines anderen Operators als einer Umwandlung oder Zuweisung zuerst in einen Typ intmit dem Wert 255 konvertiert, bevor die Operation ausgeführt wird. Deshalb erhalten sizeof(~i)Sie 4 statt 1.

In Abschnitt 6.5.3.3 wird beschrieben, dass für den ~Betreiber ganzzahlige Werbeaktionen gelten :

Das Ergebnis des ~Operators ist das bitweise Komplement seines (heraufgestuften) Operanden ( dh jedes Bit im Ergebnis wird genau dann gesetzt, wenn das entsprechende Bit im konvertierten Operanden nicht gesetzt ist). Die ganzzahligen Heraufstufungen werden für den Operanden ausgeführt, und das Ergebnis hat den heraufgestuften Typ. Wenn der heraufgestufte Typ ein vorzeichenloser Typ ist, entspricht der Ausdruck ~Edem Maximalwert, der in diesem Typ minus dargestellt werden kann E.

intWenn Sie also ein 32-Bit annehmen , wenn counteres den 8-Bit-Wert 0xffhat, wird es in den 32-Bit-Wert konvertiert 0x000000ff, und wenn Sie es anwenden ~, erhalten Sie 0xffffff00.

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.

if (!++counter) counter--;

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.

dbush
quelle
1
if (!++counter) --counter;Für manche Programmierer ist dies möglicherweise weniger seltsam als die Verwendung des Kommaoperators.
Eric Postpischil
1
Eine andere Alternative ist ++counter; counter -= !counter;.
Eric Postpischil
@EricPostpischil Eigentlich gefällt mir deine erste Option besser. Bearbeitet.
dbush
15
Das ist hässlich und unlesbar, egal wie du es schreibst. Wenn Sie eine solche Redewendung verwenden müssen, tun Sie jedem Wartungsprogrammierer einen Gefallen und schließen Sie sie als Inline-Funktion ab : so etwas wie increment_unsigned_without_wraparoundoder increment_with_saturation. Persönlich würde ich eine generische Tri-Operanden- clampFunktion verwenden.
Cody Grey
5
Sie können dies auch nicht zu einer Funktion machen, da sie sich für verschiedene Argumenttypen unterschiedlich verhalten muss. Sie müssten ein typgenerisches Makro verwenden .
user2357112 unterstützt Monica
7

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

if (~ i)

zu wissen, ob ich 255 nicht wert bin (in deinem Fall mit einem uint8_t) ist nicht sehr lesbar, tu es einfach

if (i != 255)

und Sie haben einen tragbaren und lesbaren Code


Es gibt mehrere Größen von Variablen (z. B. uint16_t und vorzeichenloses Zeichen usw.)

So verwalten Sie jede Größe von nicht signierten:

if (i != (((uintmax_t) 2 << (sizeof(i)*CHAR_BIT-1)) - 1))

Der Ausdruck ist konstant und wird zur Kompilierungszeit berechnet.

#include <limit.h> für CHAR_BIT und #include <stdint.h> für uintmax_t

Bruno
quelle
3
Die Frage besagt ausdrücklich, dass sie mehrere Größen haben, mit denen sie sich befassen müssen, und != 255ist daher unzureichend.
Eric Postpischil
@EricPostpischil ah ja, das vergesse ich, also "if (i! = ((1u << sizeof (i) * 8) - 1))" vorausgesetzt immer unsigned?
Bruno
1
Dies ist für unsignedObjekte 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.
Eric Postpischil
oh ja ofc, CHAR_BIT, mein schlechter
bruno
2
Für die Sicherheit mit breiteren Typen könnte man verwenden ((uintmax_t) 2 << sizeof(i)*CHAR_BIT-1) - 1.
Eric Postpischil
5

Hier sind verschiedene Optionen für die Implementierung von "Add 1 to xbut clamp at the maximal darstellbaren Wert", vorausgesetzt, es xhandelt sich um einen vorzeichenlosen Integer-Typ:

  1. Fügen Sie genau dann einen hinzu, wenn er xkleiner als der in seinem Typ darstellbare Maximalwert ist:

    x += x < Maximum(x);

    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.

  2. Vergleichen Sie mit dem größten Wert des Typs:

    if (x < ((uintmax_t) 2u << sizeof x * CHAR_BIT - 1) - 1) ++x

    (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. Das CHAR_BITMakro ist einigen vielleicht unbekannt. Es ist die Anzahl der Bits in einem Byte, ebenso sizeof x * CHAR_BITwie die Anzahl der Bits in der Art von x.)

    Dies kann aus Gründen der Ästhetik und Klarheit wie gewünscht in ein Makro eingeschlossen werden:

    #define Maximum(x) (((uintmax_t) 2u << sizeof (x) * CHAR_BIT - 1) - 1)
    if (x < Maximum(x)) ++x;
  3. Inkrementieren xund korrigieren Sie, wenn es auf Null gesetzt wird, indem Sie Folgendes verwenden if:

    if (!++x) --x; // !++x is true if ++x wraps to zero.
  4. Inkrementieren xund korrigieren Sie, wenn es mit einem Ausdruck auf Null gesetzt wird:

    ++x; x -= !x;

    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.

  5. Eine verzweigungslose Option unter Verwendung des obigen Makros ist:

    x += 1 - x/Maximum(x);

    Wenn xdas Maximum seines Typs ist, wird dies ausgewertet x += 1-1. Ansonsten ist es x += 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.

Eric Postpischil
quelle
1
Ich kann mich einfach nicht dazu bringen, eine Antwort zu bewerten, die die Verwendung eines Makros empfiehlt. C hat Inline-Funktionen. Innerhalb dieser Makrodefinition tun Sie nichts, was innerhalb einer Inline-Funktion nicht einfach möglich ist. Wenn Sie ein Makro verwenden möchten, stellen Sie sicher, dass Sie aus Gründen der Übersichtlichkeit strategisch in Klammern setzen: Der Operator << hat eine sehr niedrige Priorität. Clang warnt davor mit -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.
Cody Grey
1
@CodyGray, wenn Sie glauben, dass Sie dies mit einer Funktion tun können, schreiben Sie eine Antwort.
Carsten S
2
@CodyGray: sizeof xkann nicht in einer C-Funktion implementiert werden, da xdies 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.
Eric Postpischil
2

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 ....

if(~counter) counter++;

oder ... die Maske oder Typografie direkt bereitstellen

if((~counter)&0xFF) counter++;
if((uint_8)(~counter)) counter++;

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

if(sizeof(uint_8)!=1) return(FAIL);

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.

Oldtimer
quelle
0
6.5.3.3 Unäre arithmetische Operatoren
...
4 Das Ergebnis des ~Operators ist das bitweise Komplement seines ( heraufgestuften ) Operanden ( dh jedes Bit im Ergebnis wird genau dann gesetzt, wenn das entsprechende Bit im konvertierten Operanden nicht gesetzt ist ).Die ganzzahligen Heraufstufungen werden für den Operanden ausgeführt, und das Ergebnis hat den heraufgestuften Typ . Wenn der heraufgestufte Typ ein vorzeichenloser Typ ist, entspricht der Ausdruck ~Edem Maximalwert, der in diesem Typ minus dargestellt werden kann E.

C 2011 Online-Entwurf

Das Problem ist, dass der Operand von ~ wird, intbevor der Operator angewendet wird.

Leider glaube ich nicht, dass es einen einfachen Ausweg gibt. Schreiben

if ( counter + 1 ) counter++;

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:

#define MAX_COUNTER 255
...
if ( counter < MAX_COUNTER-1 ) counter++;
John Bode
quelle
Ich schätze den Punkt der Ganzzahl-Promotion - es sieht so aus, als ob dies das Problem ist, auf das wir stoßen. Hervorzuheben ist jedoch, dass in Ihrem zweiten Codebeispiel das -1nicht 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.
Charlie Salts