2016-05-16 7 views
3

Dies ist eine allgemeine Frage, aber da ich mich hauptsächlich mit gcc/g ++/VStudio beschäftige, habe ich es als c/C++ markiert. Die Frage kam mir in den Sinn, als ich mich mit Optimierungsmöglichkeiten herumärbte. Betrachten Sie in der einfachsten Form eine arithmetische Operation wie i/6 * 8. Wenn ein Mensch diesem Ausdruck gegenübersteht, wird er ihn wahrscheinlich zu etwas wie vereinfachen. Und wenn er sich mit der Multiplikation mit 4 wohler fühlt, wird er das zuerst tun, d.h. (i * 4)/3. Ich muss noch einmal betonen, dass dies nur ein einfaches Beispiel ist.Gibt es einen Fall, bei dem eine arithmetische Operation durch die Compileroptimierung beeinflusst wird?

Nun, was ist mit den Compilern? Gibt es Möglichkeiten, dass sie bei solchen Operationen dasselbe tun? Und da wir wissen, dass im obigen Beispiel i eine Ganzzahl ist, kann die Vereinfachung und Änderung der Reihenfolge der Operationen zu völlig anderen Ergebnissen führen. Die Frage kann wie folgt geändert werden: Vermeiden die Compiler solche Aktionen vollständig?

Wenn wir möchten, dass das Programm einige arithmetische Operationen genau so ausführt, wie wir es angegeben haben, und ohne die Reihenfolge der Operationen zu ändern, sollten wir uns über das Verhalten des Compilers Sorgen machen?

+3

Ein optimierender Compiler ändert den angegebenen Code in den schnellsten Code, den er kennt, der die Garantien der Sprache beibehält. – xaxxon

+0

Mit anderen Worten, "sollten wir uns Sorgen um das Verhalten des Compilers machen?" Nein, * wenn * Sie gültigen Code schreiben. UB-Code in eine andere Sache zu transformieren ist absolut gültig und passiert in der Realität. – deviantfan

+0

Ich weiß. Aber ich will sicher sein. Der Titel der Frage fragt, ob sich jemand mit solchen Vorfällen konfrontiert sieht. –

Antwort

3

Wie andere Antworten erklärt haben, müssen gültige Compiler konservativ sein und dürfen keine Optimierungen verwenden, die das Verhalten gut definierter Programme verändern würden. Aber es ist wichtig, sich daran zu erinnern, dass dieser Konservatismus nur für gültige, richtig geschriebene, wohldefinierte Programme gilt. Wenn der kompilierte Code von einem undefinierten Verhalten abhängt, können moderne Compiler geradezu radikal in den Optimierungen sein, die sie verwenden, und in der realen Welt bedeutet dies, dass die Antwort auf die Frage tatsächlich lautet: "Ja, es gibt Fälle, in denen Eine arithmetische Operation kann durch die Compileroptimierung beeinflusst werden. "

Hier sind zwei große Web-Seiten einige der Bedeutung verändernden Optimierungen beschreiben, die manchmal Compiler gelten, wenn mit undefiniertem Verhalten konfrontiert:

Eine Programmiersprache Definition wird oft als ein "Vertrag" zwischen dem Programmierer und das Programm als eine Partei und der Compiler und seine Implementoren als die andere. Solange Ihr Code allen Regeln folgt, ist der Compiler verpflichtet, eine ausführbare Datei mit einem Verhalten zu erzeugen, das genau der Sprachdefinition und der "abstrakten Maschine" entspricht. Aber wenn Sie irgendwelche Regeln brechen, und insbesondere wenn Ihr Code in ein undefiniertes Verhalten wandert, sind alle Wetten aus, der Vertrag ist ungültig, und der Compiler kann grundsätzlich tun, was er will.

Zum Beispiel, wenn Sie

int i = 1; 
printf("%d\n", i++ + i++); /* WRONG */ 

schreiben werden Sie sehr wahrscheinlich feststellen, dass der Wert des Ausdrucks ändert, wie Sie die Optimierungsstufe ändern.

(Unnötig zu sagen, die Moral der Geschichte ist nicht, "Wenn Sie undefined Code schreiben, müssen Sie vorsichtig sein, welche Optimierungseinstellungen Sie verwenden." Die richtige Lektion zu lernen ist, "Don ' t) Code schreiben, der von undefiniertem Verhalten abhängt. ")

6

Am wahrscheinlichsten würde der Compiler "konstante Faltung" und "konstante Propagierung" -Optimierungen auf konstanten Ausdruck anwenden.

Im obigen Fall kann der Compiler solche Optimierungen nicht anwenden.

i = i * (4/2) 

Imagine Der Compiler

i= i * 2 

Dies ist wegen der konstanten Faltung erzeugen würde.

5

Compiler sind sehr konservativ, wenn es darum geht, den Code zu optimieren. Sie können die Reihenfolge ändern, in der Operationen ausgeführt werden, und sogar arithmetische Operationen vorberechnen, deren Operanden zur Kompilierungszeit bekannt sind (dies wird als Konstantenfaltung bezeichnet), aber sie werden niemals das berechnete Ergebnis ändern. Fließkommaoperationen sind ein bisschen problematischer. Sie können im Allgemeinen die Reihenfolge der Berechnung nicht ändern oder vorberechnen, ohne das berechnete Ergebnis zu ändern. Daher belassen die meisten Compiler sie unverändert. Es ist jedoch möglich, den Compiler zu bitten, aggressiv zu optimieren; In diesem Fall wird sich das berechnete Ergebnis wahrscheinlich ändern, aber der Benutzer hat danach gefragt. Dies ist beispielsweise der Fall der gcc-Option -Ofast (weil intern die Optionen -ffast-math gesetzt werden). Beachten Sie, dass es zu seltsamen Nebenwirkungen wie unerwartete "zufällige" Division durch Nullen führen kann.

** Edit: Hinweis auf nicht-arithmetische Operationen **

Optimierungen erhalten noch schwieriger, wenn die Code-Zeiger und Funktionsaufrufe enthalten. Es ist im Allgemeinen unmöglich, Nebenwirkungen vorherzusagen (denken Sie an Zeiger-Aliasing und globale Variablen). Deshalb geben Compiler immer in einem sehr konservativen Weg auf: ein gut kompiliertes Programm sollte zumindest stimmen, schnell sein ist ein Luxus.

** Edit: einige Beispiele **

Diese Frage SO ein sehr detailliertes Beispiel für das gibt, was mit Gleitkomma passieren kann: Different floating point result with optimization enabled - compiler bug?

+3

Ein sehr wichtiger Vorbehalt, zäh: moderne Compiler sind "sehr konservativ" * nur, wenn der Code, den sie kompilieren, klar definiert ist *. Aber wenn der Code undefiniertes Verhalten enthält, sind alle Wetten deaktiviert, und Compiler, die aggressive Optimierungen verwenden, haben nichts dagegen, wenn sie das Verhalten des Programms ändern, da das Verhalten überhaupt nicht definiert wurde. –

4

Es gemeinsam zwischen Gleichung Vereinfachung und Compiler-Optimierung sehr wenig ist.Erstere zielt darauf ab, einen Ausdruck für den Menschen lesbarer zu machen, letzterer zielt darauf ab, ein Programm so effizient wie möglich zu machen. Die Vereinfachung der Gleichung, wie Sie es getan haben, würde kein schnelleres Programm ergeben, so dass der Compiler sich nicht darum kümmern wird.

Der Compiler kann den Ausdruck nicht in i * 8/6 umordnen, da dies die Bedeutung des Codes ändern könnte. Im Grunde genommen ist ein Compiler viel schlauer als ein menschlicher Mathematiker, weil der Compiler sich der Typen voll bewusst ist, während einem Menschen diese Art von Bewusstsein fehlt. Bei der Programmierung ist i/6 * 8nicht entspricht i * 8/6! Weil es das Problem eines möglichen Integer-Überlaufs gibt. Wenn der Compiler nicht weiß, welchen Wert i haben wird, dann könnte die Neuordnung einen Überlauf verursachen, wenn i * 8 nicht in eine Ganzzahl passen kann.

Aus dem gleichen Grund kann der Compiler auch nicht zu Code zu wechseln. Was ist, wenn der Programmierer einen Überlauf wollte? Das Programm könnte ein Versuch sein, undefiniertes Verhalten zu demonstrieren, oder es könnte ein Compiler-Verhalten für den Fall eines Überlaufs implementiert werden. Wenn der Compiler die Werte ändern würde, würde möglicherweise kein Überlauf mehr auftreten und das Programmverhalten würde sich ändern, was nicht erlaubt ist.

Wahrscheinlicher ist, dass der Compiler nach einer Möglichkeit sucht, eine der Operationen durch Vorberechnung zur Kompilierungszeit zu entfernen. Und es wird vielleicht auch nach einer Möglichkeit gesucht, die Division durch Bitverschiebungen zu ersetzen, da die Division traditionell eine langsame Operation ist. Welche Optimierungen tatsächlich vorgenommen werden, hängt vom gesamten umgebenden Code ab.