(A + B + C) ≠ (A + C + B) und Compiler-Neuordnung


108

Das Hinzufügen von zwei 32-Bit-Ganzzahlen kann zu einem Ganzzahlüberlauf führen:

uint64_t u64_z = u32_x + u32_y;

Dieser Überlauf kann vermieden werden, wenn eine der 32-Bit-Ganzzahlen zuerst umgewandelt oder zu einer 64-Bit-Ganzzahl hinzugefügt wird.

uint64_t u64_z = u32_x + u64_a + u32_y;

Wenn der Compiler jedoch beschließt, den Zusatz neu zu ordnen:

uint64_t u64_z = u32_x + u32_y + u64_a;

Der ganzzahlige Überlauf kann immer noch auftreten.

Dürfen Compiler eine solche Neuordnung durchführen oder können wir darauf vertrauen, dass sie die Ergebnisinkonsistenz bemerken und die Ausdrucksreihenfolge unverändert lassen?


15
Sie zeigen keinen ganzzahligen Überlauf an, da Sie scheinbar zusätzliche uint32_tWerte sind - die nicht überlaufen, sondern umbrechen. Dies sind keine unterschiedlichen Verhaltensweisen.
Martin Bonner unterstützt Monica

5
In Abschnitt 1.9 der c ++ - Standards wird Ihre Frage direkt beantwortet (es gibt sogar ein Beispiel, das fast genau Ihrem entspricht).
Holt

3
@Tal: Wie andere bereits gesagt haben: Es gibt keinen Überlauf von ganzen Zahlen. Unsigned ist als Wrap definiert, für Signed ist es ein undefiniertes Verhalten, sodass jede Implementierung, einschließlich Nasendämonen, funktioniert.
zu ehrlich für diese Seite

5
@ Tal: Unsinn! Wie ich bereits schrieb: Der Standard ist sehr klar und erfordert eine Umhüllung, nicht eine Sättigung (das wäre mit signiert möglich, da dies UB ab dem Standard ist.
zu ehrlich für diese Seite

15
@rustyx: Ob Sie nennen es umhüllt oder überfüllt, die Punkt bleibt , dass ((uint32_t)-1 + (uint32_t)1) + (uint64_t)0Ergebnisse in 0, während (uint32_t)-1 + ((uint32_t)1 + (uint64_t)0)Ergebnisse in 0x100000000, und diese beiden Werte nicht gleich sind. Es ist also wichtig, ob der Compiler diese Transformation anwenden kann oder nicht. Aber ja, der Standard verwendet das Wort "Überlauf" nur für vorzeichenbehaftete Ganzzahlen, nicht für vorzeichenlose.
Steve Jessop

Antworten:


84

Wenn der Optimierer eine solche Neuordnung vornimmt, ist er immer noch an die C-Spezifikation gebunden, sodass eine solche Neuordnung zu Folgendem werden würde:

uint64_t u64_z = (uint64_t)u32_x + (uint64_t)u32_y + u64_a;

Begründung:

Wir beginnen mit

uint64_t u64_z = u32_x + u64_a + u32_y;

Die Addition erfolgt von links nach rechts.

Die Regeln für die Ganzzahl-Heraufstufung besagen, dass beim ersten Hinzufügen im ursprünglichen Ausdruck u32_xzu befördert werden soll uint64_t. In der zweiten Ergänzung u32_ywird auch befördert uint64_t.

Also, um mit der C - Spezifikation kompatibel zu sein, muss jede optimiser zu fördern u32_xund u32_yzu 64 - Bit - Werten ohne Vorzeichen. Dies entspricht dem Hinzufügen einer Besetzung. (Die eigentliche Optimierung erfolgt nicht auf C-Ebene, aber ich verwende die C-Notation, da dies eine Notation ist, die wir verstehen.)


Ist es nicht linksassoziativ (u32_x + u32_t) + u64_a?
Nutzlos

12
@ Nutzlos: Klas hat alles auf 64 Bit gegossen. Jetzt macht die Bestellung überhaupt keinen Unterschied. Der Compiler muss nicht der Assoziativität folgen, sondern muss genau das gleiche Ergebnis erzielen, als ob dies der Fall wäre.
Gnasher729

2
Es scheint darauf hinzudeuten, dass der OP-Code so ausgewertet wird, was nicht stimmt.
Nutzlos

@Klas - möchten Sie erklären, warum dies der Fall ist und wie genau Sie zu Ihrem Codebeispiel gelangen?
Rustyx

1
@rustyx Es brauchte eine Erklärung. Danke, dass du mich dazu gedrängt hast, einen hinzuzufügen.
Klas Lindbäck

28

Ein Compiler darf nur nach der As- If- Regel nachbestellen . Das heißt, wenn die Neuordnung immer das gleiche Ergebnis wie die angegebene Reihenfolge liefert, ist dies zulässig. Ansonsten (wie in Ihrem Beispiel) nicht.

Zum Beispiel mit dem folgenden Ausdruck

i32big1 - i32big2 + i32small

Der Compiler wurde sorgfältig konstruiert, um die beiden als groß, aber ähnlich bekannten Werte zu subtrahieren und erst dann den anderen kleinen Wert zu addieren (wodurch ein Überlauf vermieden wird).

(i32small - i32big2) + i32big1

und verlassen Sie sich auf die Tatsache, dass die Zielplattform Zwei-Komplement-Arithmetik mit Wrap-Round verwendet, um Probleme zu vermeiden. (Eine solche Neuordnung kann sinnvoll sein, wenn der Compiler auf Register gedrückt wird und sich zufällig bereits i32smallin einem Register befindet.)


Das Beispiel von OP verwendet vorzeichenlose Typen. i32big1 - i32big2 + i32smallimpliziert signierte Typen. Zusätzliche Bedenken kommen ins Spiel.
chux

@chux Absolut. Der Punkt, den ich ansprechen wollte, ist, dass (i32small-i32big2) + i32big1der Compiler , obwohl ich nicht schreiben konnte (weil dies UB verursachen könnte), es effektiv neu anordnen kann, weil der Compiler sicher sein kann, dass das Verhalten korrekt ist.
Martin Bonner unterstützt Monica

3
@chux: Zusätzliche Bedenken wie UB kommen nicht ins Spiel, da es sich um eine Neuordnung des Compilers nach der Als-ob-Regel handelt. Ein bestimmter Compiler kann die Kenntnis seines eigenen Überlaufverhaltens nutzen.
MSalters

16

In C, C ++ und Objective-C gibt es die "Als ob" -Regel: Der Compiler kann tun, was er will, solange kein konformes Programm den Unterschied erkennen kann.

In diesen Sprachen ist a + b + c definiert als (a + b) + c. Wenn Sie den Unterschied zwischen diesem und beispielsweise a + (b + c) erkennen können, kann der Compiler die Reihenfolge nicht ändern. Wenn Sie den Unterschied nicht erkennen können, kann der Compiler die Reihenfolge ändern, aber das ist in Ordnung, da Sie den Unterschied nicht erkennen können.

In Ihrem Beispiel könnte der Compiler mit b = 64 Bit, a und c 32 Bit (b + a) + c oder sogar (b + c) + a auswerten, da Sie den Unterschied aber nicht erkennen konnten nicht (a + c) + b, weil man den Unterschied erkennen kann.

Mit anderen Worten, der Compiler darf nichts tun, was dazu führt, dass sich Ihr Code anders verhält als er sollte. Es wird nicht um den Code zu produzieren erforderlich , dass Sie es produzieren würde denken, oder dass Sie denken , es sollte produzieren, aber der Code wird Ihnen genau die Ergebnisse es sollte.


Aber mit einer großen Einschränkung; Dem Compiler steht es frei, kein undefiniertes Verhalten anzunehmen (in diesem Fall Überlauf). Dies ähnelt if (a + 1 < a)der Optimierung einer Überlaufprüfung .
csiz

7
@csiz ... auf signierten Variablen. Vorzeichenlose Variablen haben eine genau definierte Überlaufsemantik (Wrap-Around).
Gavin S. Yancey

7

Zitat aus den Standards :

[Hinweis: Operatoren können nur dann nach den üblichen mathematischen Regeln neu gruppiert werden, wenn die Operatoren wirklich assoziativ oder kommutativ sind.7 Zum Beispiel im folgenden Fragment int a, b;

/∗ ... ∗/
a = a + 32760 + b + 5;

Die Ausdrucksanweisung verhält sich genauso wie

a = (((a + 32760) + b) + 5);

aufgrund der Assoziativität und Vorrang dieser Operatoren. Somit wird das Ergebnis der Summe (a + 32760) als nächstes zu b addiert, und dieses Ergebnis wird dann zu 5 addiert, was zu dem Wert führt, der a zugewiesen ist. Auf einer Maschine, auf der Überläufe eine Ausnahme erzeugen und auf der der durch ein int darstellbare Wertebereich [-32768, + 32767] beträgt, kann die Implementierung diesen Ausdruck nicht umschreiben als

a = ((a + b) + 32765);

denn wenn die Werte für a und b -32754 bzw. -15 wären, würde die Summe a + b eine Ausnahme erzeugen, während der ursprüngliche Ausdruck dies nicht tun würde; Der Ausdruck kann auch nicht als neu geschrieben werden

a = ((a + 32765) + b);

oder

a = (a + (b + 32765));

da die Werte für a und b jeweils 4 und -8 oder -17 und 12 gewesen sein könnten. Auf einer Maschine, bei der Überläufe keine Ausnahme erzeugen und bei der die Ergebnisse von Überläufen reversibel sind, kann die obige Ausdrucksanweisung von der Implementierung auf eine der oben genannten Arten umgeschrieben werden, da das gleiche Ergebnis erzielt wird. - Endnote]


4

Dürfen Compiler eine solche Neuordnung durchführen oder können wir darauf vertrauen, dass sie die Ergebnisinkonsistenz bemerken und die Ausdrucksreihenfolge unverändert lassen?

Der Compiler kann nur dann neu anordnen, wenn er das gleiche Ergebnis liefert - hier, wie Sie festgestellt haben, nicht.


Es ist möglich, eine Funktionsvorlage zu schreiben, wenn Sie eine möchten, die alle Argumente std::common_typevor dem Hinzufügen heraufstuft - dies wäre sicher und würde sich weder auf die Reihenfolge der Argumente noch auf das manuelle Casting stützen, aber es ist ziemlich klobig.


Ich weiß, dass explizites Casting verwendet werden sollte, aber ich möchte das Verhalten des Compilers kennen, wenn ein solches Casting fälschlicherweise weggelassen wurde.
Tal

1
Wie gesagt, ohne explizites Casting: Die linke Addition wird zuerst ohne integrale Werbung durchgeführt und unterliegt daher einer Verpackung. Das Ergebnis dieser Addition, möglicherweise gewickelt wird dann gefördert uint64_tfür die Zugabe zu dem am weitesten rechts stehenden Wert.
Nutzlos

Ihre Erklärung zur Als-ob-Regel ist völlig falsch. Die Sprache C gibt beispielsweise an, welche Operationen auf einem abstrakten Computer ausgeführt werden sollen. Die "als ob" -Regel erlaubt es ihm, absolut zu tun, was er will, solange niemand den Unterschied erkennen kann.
Gnasher729

Dies bedeutet, dass der Compiler tun kann, was er will, solange das Ergebnis das gleiche ist, das durch die gezeigten Regeln für Linksassoziativität und arithmetische Konvertierung bestimmt wird.
Nutzlos

1

Es hängt von der Bitbreite von ab unsigned/int.

Die folgenden 2 sind nicht gleich (wenn unsigned <= 32Bits). u32_x + u32_ywird 0.

u64_a = 0; u32_x = 1; u32_y = 0xFFFFFFFF;
uint64_t u64_z = u32_x + u64_a + u32_y;
uint64_t u64_z = u32_x + u32_y + u64_a;  // u32_x + u32_y carry does not add to sum.

Sie sind gleich (wenn unsigned >= 34Bits). Integer-Promotions führten u32_x + u32_ydazu, dass bei 64-Bit-Mathematik Additionen auftraten. Reihenfolge ist irrelevant.

Es ist UB (wenn unsigned == 33Bits). Ganzzahlige Promotions führten dazu, dass bei signierter 33-Bit-Mathematik eine Addition auftrat und der signierte Überlauf UB ist.

Dürfen Compiler eine solche Neuordnung vornehmen ...?

(32-Bit-Mathematik): Ja neu anordnen , aber es müssen die gleichen Ergebnisse erzielt werden, damit nicht die Neuanordnung von OP vorgeschlagen wird. Unten sind die gleichen

// Same
u32_x + u64_a + u32_y;
u64_a + u32_x + u32_y;
u32_x + (uint64_t) u32_y + u64_a;
...

// Same as each other below, but not the same as the 3 above.
uint64_t u64_z = u32_x + u32_y + u64_a;
uint64_t u64_z = u64_a + (u32_x + u32_y);

... können wir darauf vertrauen, dass sie die Ergebnisinkonsistenz bemerken und die Ausdrucksreihenfolge unverändert lassen?

Vertrauen Sie ja, aber das Codierungsziel von OP ist nicht ganz klar. Sollte u32_x + u32_ytragen tragen? Wenn OP diesen Beitrag wünscht, sollte Code sein

uint64_t u64_z = u64_a + u32_x + u32_y;
uint64_t u64_z = u32_x + u64_a + u32_y;
uint64_t u64_z = u32_x + (u32_y + u64_a);

Aber nicht

uint64_t u64_z = u32_x + u32_y + u64_a;
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.