Können unterschiedliche Optimierungsstufen zu funktional unterschiedlichem Code führen?

Lesezeit: 10 Minuten

Benutzeravatar von Kerrek SB
Kerrek SB

Mich interessiert, welche Freiheiten ein Compiler bei der Optimierung hat. Beschränken wir diese Frage auf GCC und C/C++ (jede Version, jede Art von Standard):

Ist es möglich, Code zu schreiben, der verhält unterschiedlich, je nachdem mit welcher Optimierungsstufe kompiliert wurde?

Das Beispiel, an das ich denke, ist das Drucken verschiedener Textteile in verschiedenen Konstruktoren in C++ und das Erhalten eines Unterschieds, je nachdem, ob Kopien entfernt werden (obwohl ich nicht in der Lage war, so etwas zum Laufen zu bringen).

Das Zählen von Taktzyklen ist nicht erlaubt. Wenn Sie ein Beispiel für einen Nicht-GCC-Compiler haben, wäre ich auch neugierig, aber ich kann es nicht überprüfen. Bonuspunkte für ein Beispiel in C. 🙂

Bearbeiten: Der Beispielcode sollte standardkonform sein und von vornherein kein undefiniertes Verhalten enthalten.

Bearbeiten 2: Habe schon tolle Antworten bekommen! Lassen Sie mich den Einsatz noch etwas erhöhen: Der Code muss ein wohlgeformtes Programm darstellen und standardkonform sein, und er muss in jeder Optimierungsstufe zu korrekten, deterministischen Programmen kompilieren. (Das schließt Dinge wie Race-Conditions in schlecht geformtem Multithread-Code aus.) Ich weiß auch, dass die Gleitkommarundung betroffen sein kann, aber lassen Sie uns das außer Acht lassen.

Ich habe gerade 800 Ruf erreicht, also denke ich, dass ich 50 Ruf als Prämie auf das erste vollständige Beispiel ausgeben werde, das (dem Geist) dieser Bedingungen entspricht; 25, wenn es um den Missbrauch von striktem Aliasing geht. (Vorbehaltlich jemandem, der mir zeigt, wie man ein Kopfgeld an jemand anderen sendet.)

  • Beachten Sie, dass im Falle des Kopierens der Compiler der Compiler ist ausdrücklich erlaubt um das beobachtbare Verhalten des Programms zu ändern.

    – Robᵩ

    15. Juni 2011 um 21:14 Uhr

  • @Rob: Das ist in Ordnung, ich würde mich sehr freuen, nur ein funktionierendes Beispiel dafür zu sehen. Ich verstehe, dass von allen Konstruktoren erwartet wird, dass sie semantisch identische Objekte liefern. Indem ich Druckroutinen in sie einfüge, führe ich absichtlich eine Unterscheidung ein, um die sich der Compiler nicht kümmern muss. Das wäre aber ein akzeptables Beispiel!

    – Kerrek SB

    15. Juni 2011 um 21:17 Uhr

  • “Ich konnte so etwas nicht zum Laufen bringen” – vielleicht ist in GCC die Kopier-Ctor-Eliminierung auch ohne Optimierung aktiviert?

    – Steve Jessop

    15. Juni 2011 um 21:18 Uhr


  • @Kerrek: aha! man zur rettung, ja kannst du, mit -fno-elide-constructors. Angenommen, Sie erlauben eine so feinkörnige Option wie “andere Optimierungsstufe”, ich denke, das passt zu Ihren Anforderungen.

    – Steve Jessop

    15. Juni 2011 um 21:43 Uhr


  • @Kerrek: Vermutlich könnten Sie Probleme haben, wenn der Satz versucht, einen Wert, der irgendwie in einem 80-Bit-FPU-Register bis zu seiner Verwendung überlebt hat, mit “demselben Wert”, der im Speicher gespeichert wurde, zu vergleichen den Weg und wurde daher abgeschnitten. Dies könnte zu einer Unmöglichkeit führen, dass die std::set Umsetzung nicht vorausgesehen. Ich bin mir nicht sicher, ob dies standardkonform ist, wohlgemerkt, aber PlasmaHH spricht von einer vom Standard abweichenden Realität, dh einem Fehler, also geht wirklich alles 🙂

    – Steve Jessop

    17. August 2011 um 18:18 Uhr


Benutzeravatar von Robᵩ
Robᵩ

Der anzuwendende Teil des C++-Standards ist §1.9 “Programmausführung”. Es lautet auszugsweise:

konforme Implementierungen sind erforderlich, um (nur) das beobachtbare Verhalten der abstrakten Maschine zu emulieren, wie unten erläutert. …

Eine konforme Implementierung, die ein wohlgeformtes Programm ausführt, muss dasselbe beobachtbare Verhalten erzeugen wie eine der möglichen Ausführungssequenzen der entsprechenden Instanz der abstrakten Maschine mit demselben Programm und derselben Eingabe. …

Das beobachtbare Verhalten der abstrakten Maschine ist ihre Abfolge von Lese- und Schreibvorgängen für flüchtige Daten und Aufrufe von Bibliotheks-E/A-Funktionen. …

Also, ja, Code darf sich verhalten unterschiedlich auf verschiedenen Optimierungsstufen, aber (unter der Annahme, dass alle Stufen einen konformen Compiler produzieren), aber sie können sich nicht verhalten bemerkbar anders.

BEARBEITEN: Erlauben Sie mir, meine Schlussfolgerung zu korrigieren: Ja, Code kann sich auf verschiedenen Optimierungsebenen unterschiedlich verhalten, solange jedes Verhalten beobachtbar identisch mit einem der Verhaltensweisen der abstrakten Maschine des Standards ist.

  • Wie passt das zum Aufruf verschiedener Konstruktoren? Wenn ich schreibe T x /* default */; x = T(2, 3);wäre es illegal, dies zu optimieren T x(2, 3);?

    – Kerrek SB

    15. Juni 2011 um 21:32 Uhr

  • Beachten Sie “eine der möglichen Ausführungsreihenfolgen”. Es muss nicht sein gleich eine auf jeder Optimierungsstufe. So darf der Compiler beispielsweise Funktionsargumente in einer Reihenfolge bei -O0 und in einer anderen Reihenfolge bei -O4 auswerten. Ein Programm, dessen Ausgabe je nach tatsächlicher Reihenfolge unterschiedlich ist, ist gültig, obwohl es nicht das ist, was der C-Standard als “streng konformes Programm” bezeichnet. Dann wieder printf("%d\n", CHAR_BIT) ist auch kein streng konformes Programm, da die Ausgabe implementierungsdefiniert ist.

    – Steve Jessop

    15. Juni 2011 um 21:35 Uhr

  • @Kerrek: Diese Optimierung ist beispielsweise beim Zuweisungsoperator illegal T::operator=(const T &) hat beobachtbare Nebenwirkungen. Wie Sie wissen, gibt es eine spezielle Regel, die das Entfernen des Kopierctors erlaubt, selbst wenn der Kopierctor Nebeneffekte hat. Es gibt keine solche Regel für die Zuweisung von Kopien. Gleiches gilt für die Default-Konstruktion, deren beobachtbares Verhalten ebenfalls nicht ausgeblendet werden kann. Aber solange nichts das beobachtbare Verhalten beeinflusst, kann der Compiler alles weglassen, was er will (und wenn x unbenutzt ist, überhaupt kein Objekt konstruieren).

    – Steve Jessop

    15. Juni 2011 um 21:36 Uhr


  • @Steve: Ich wusste eigentlich nichts über die Ausnahme für den Kopierer. Bedeutet das, dass die Unbestimmtheit, wie oft etwas kopiert (oder standardmäßig erstellt) wird, die nur etwas, das das sichtbare Verhalten gemäß der Norm beeinflussen kann?

    – Kerrek SB

    15. Juni 2011 um 21:46 Uhr

  • @Kerrek: Sie wussten es, auch wenn Sie nicht wussten, dass es sich um einen expliziten Sonderfall handelte, da Sie in der Frage die Elision des Kopierctors erwähnt haben :-). Das ist nicht das Einzige: Alles, was nicht spezifiziert oder durch die Implementierung definiert ist, kann sich ändern. Die verwendete Sprache ist 12.8/15 und lautet “Die Implementierung darf dies tun”, aber in Wirklichkeit ist das, was es sagt, dasselbe, als ob es hieße: “Es ist nicht angegeben, ob die Implementierung dies tut.”

    – Steve Jessop

    15. Juni 2011 um 21:51 Uhr


Fließkommaberechnungen sind eine reife Quelle für Differenzen. Je nach Reihenfolge der einzelnen Operationen kann es zu mehr/weniger Rundungsfehlern kommen.

Weniger als sicherer Multithread-Code kann auch unterschiedliche Ergebnisse haben, je nachdem, wie die Speicherzugriffe optimiert sind, aber das ist im Wesentlichen sowieso ein Fehler in Ihrem Code.

Und wie Sie bereits erwähnt haben, können Nebenwirkungen in Kopierkonstruktoren verschwinden, wenn sich die Optimierungsstufen ändern.

  • Ich möchte solches Multithreading-Zeug ausschließen, weil das im Wesentlichen nur undefiniertes Verhalten ist – Sie dürfen nur ein Multithreading-Beispiel verwenden, wenn es ordnungsgemäß synchronisiert wird. Ich hatte nicht über Gleitkomma-Ungenauigkeiten nachgedacht, lassen Sie uns diese auch außer Acht lassen (obwohl das ein fairer Punkt ist).

    – Kerrek SB

    15. Juni 2011 um 21:20 Uhr

  • Compiler dürfen Gleitkommaoperationen nicht neu anordnen, es sei denn, Sie verwenden “unsichere” Optimierungsoptionen wie -ffast-math. (Sie dürfen sich “zusammenziehen” a*b + c hinein fma(a,b,c) tun dies jedoch in der Praxis, wenn Sie auf eine CPU abzielen, die FMA-Anweisungen enthält.)

    – Peter Cordes

    24. Juni um 20:32 Uhr

  • Abgesehen davon sind Gleitkomma-Rundungsdifferenzen normalerweise nur bei Implementierungen wie 32-Bit-x86 mit dem x87-FP vorhanden, bei denen Temporäre im Register eine höhere Genauigkeit als haben double. dh FLT_EVAL_METHOD == 2außer sogar über Anweisungen hinweg (nicht nur innerhalb eines Ausdrucks), es sei denn, Sie verwenden gcc -ffloat-store oder -O0. Siehe auch randomascii.wordpress.com/2012/03/21/…. Das gehört jedoch größtenteils der Vergangenheit an, da SSE2 für skalare Mathematik auf x86, wie die meisten anderen ISAs, deterministisches FP mit hat FLT_EVAL_METHOD == 0.

    – Peter Cordes

    24. Juni um 20:33 Uhr


Benutzeravatar von BЈовић
BЈовић

Ist es möglich, Code zu schreiben, der sich unterschiedlich verhält, je nachdem, mit welcher Optimierungsstufe er kompiliert wurde?

Nur wenn Sie einen Compiler-Bug auslösen.

BEARBEITEN

Dieses Beispiel verhält sich auf gcc 4.5.2 anders:

void foo(int i) {
  foo(i+1);
}

main() {
  foo(0);
}

Kompiliert mit -O0 erzeugt ein Programm, das mit einem Segmentierungsfehler abstürzt.
Kompiliert mit -O2 erstellt ein Programm, das in eine Endlosschleife eintritt.

  • Ihr Beispiel ist jedoch kein Compiler-Fehler, es funktioniert gut mit der Optimierung, da gcc die Tail-Call-Optimierung durchführt, es stürzt ohne Optimierung ab, weil Ihnen der Stapelspeicher ausgeht.

    – Nr

    15. Juni 2011 um 21:19 Uhr


  • @nos: Zu keinem Zeitpunkt gibt das OP an, dass er nach Compiler-Fehlern sucht.

    – Dennis Zickefoose

    15. Juni 2011 um 21:25 Uhr

  • Das obige ist definitiv kein Compiler-Bug. Es hört sich so an, als ob der Code in beiden Fällen perfekt funktioniert.

    – Martin York

    15. Juni 2011 um 22:34 Uhr

  • Nicht nur, wenn Sie einen Fehler auslösen. Ein Beispiel: int x = printf("hello") + printf("world"); Es könnte drucken helloworld oder worldhellound was es tut könnte hängt davon ab, ob die Optimierung aktiviert ist. Das heißt, Optimierung könnte Dinge ändern, wo das Verhalten ist nicht spezifiziert.

    – R.. GitHub HÖR AUF, EIS ZU HELFEN

    16. Juni 2011 um 2:10 Uhr

  • Das Beispiel hat mir gefallen, da es tatsächlich eine bestimmte Optimierung zeigt Schwanzrekursion an Ort und Stelle. Im ersten Fall wird die Optimierung nicht angewendet und erzeugt einen Stapelüberlauf, indem sie sich selbst rekursiv aufruft, während im zweiten Beispiel der Optimierer die Rekursion in eine Schleife umgewandelt hat … wieder Großartig Beispiel.

    – David Rodríguez – Dribeas

    17. August 2011 um 18:15 Uhr

Benutzeravatar von Steve Jessop
Steve Jessop

OK, mein schamloses Spiel um das Kopfgeld, indem ich ein konkretes Beispiel gebe. Ich werde die Teile aus den Antworten anderer Leute und meinen Kommentaren zusammenstellen.

Zum Zwecke des unterschiedlichen Verhaltens bei unterschiedlichen Optimierungsstufen soll “Optimierungsstufe A” bezeichnen gcc -O0 (Ich verwende Version 4.3.4, aber es spielt keine große Rolle, ich denke, jede auch nur vage aktuelle Version wird den Unterschied zeigen, nach dem ich suche) und “Optimierungsstufe B” soll bezeichnen gcc -O0 -fno-elide-constructors.

Code ist einfach:

#include <iostream>

struct Foo {
    ~Foo() { std::cout << "~Foo\n"; }
};

int main() {
    Foo f = Foo();
}

Ausgabe auf Optimierungsstufe A:

~Foo

Ausgabe auf Optimierungsstufe B:

~Foo
~Foo

Der Code ist völlig legal, aber die Ausgabe ist implementierungsabhängig, da der Kopierkonstruktor entfernt wird, und insbesondere reagiert er empfindlich auf das Optimierungs-Flag von gcc, das das Entfernen des Kopierctors deaktiviert.

Beachten Sie, dass sich „Optimierung“ im Allgemeinen auf Compiler-Transformationen bezieht, die undefiniertes, nicht spezifiziertes oder implementierungsdefiniertes Verhalten ändern können, jedoch kein durch den Standard definiertes Verhalten. Jedes Beispiel, das Ihre Kriterien erfüllt, ist also zwangsläufig ein Programm, dessen Ausgabe entweder nicht spezifiziert oder implementierungsdefiniert ist. In diesem Fall ist es vom Standard nicht spezifiziert, ob Kopierschutze entfernt werden, ich habe nur Glück, dass GCC sie so ziemlich zuverlässig eliminiert, wann immer es erlaubt ist, aber eine Option hat, dies zu deaktivieren.

Benutzeravatar von Jens Gustedt
Jens Gustedt

Für C sind fast alle Operationen streng in der abstrakten Maschine definiert und Optimierungen sind nur erlaubt, wenn das beobachtbare Ergebnis genau das dieser abstrakten Maschine ist. Ausnahmen von dieser Regel, die mir in den Sinn kommen:

  • undefiniertes Verhalten muss zwischen verschiedenen Compilerläufen oder Ausführungen des fehlerhaften Codes nicht konsistent sein
  • Gleitkommaoperationen können zu unterschiedlichen Rundungen führen
  • Argumente für Funktionsaufrufe können in beliebiger Reihenfolge ausgewertet werden
  • Ausdrücke mit volatile Qualifizierte Typen können nur auf ihre Nebenwirkungen hin bewertet werden oder auch nicht
  • identisch const Qualifizierte zusammengesetzte Literale können in eine statische Speicherstelle gefaltet werden oder nicht

Benutzeravatar von ninjalj
ninjalj

Alles, was gemäß dem Standard undefiniertes Verhalten ist, kann sein Verhalten je nach Optimierungsstufe (oder Mondphase, für diese Angelegenheit) ändern.

Benutzeravatar von Kleist
Kleist

Da Kopierkonstruktoraufrufe wegoptimiert werden können, selbst wenn sie Nebeneffekte haben, führt das Vorhandensein von Kopierkonstruktoren mit Nebeneffekten dazu, dass sich nicht optimierter und optimierter Code unterschiedlich verhalten.

  • Ja, das war von Anfang an meine Idee, es ist nur so, dass ich es nie geschafft habe, tatsächlich ein unterschiedliches Verhalten durch Variieren der Optimierungsstufe zu erreichen. Steve Jessop entdeckt -fno-elide-constructors obwohl, was den Trick tut.

    – Kerrek SB

    15. Juni 2011 um 22:04 Uhr

1405400cookie-checkKönnen unterschiedliche Optimierungsstufen zu funktional unterschiedlichem Code führen?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy