Woher weiß ich, ob der Compiler meinen Code beschädigt hat und was ich tun soll, wenn es sich um den Compiler handelt?

14

Hin und wieder funktioniert C ++ - Code nicht, wenn er mit einer gewissen Optimierungsstufe kompiliert wird. Es kann sich um einen Compiler handeln, der eine Optimierung durchführt, die den Code bricht, oder um Code, der ein undefiniertes Verhalten enthält, das es dem Compiler ermöglicht, das zu tun, was er für richtig hält.

Angenommen, ich habe einen Code, der beim Kompilieren nur mit höheren Optimierungsstufen kaputt geht. Woher weiß ich, ob es sich um den Code oder den Compiler handelt und was mache ich, wenn es sich um den Compiler handelt?

scharfer Zahn
quelle
43
Höchstwahrscheinlich bist du es.
Littleadv
9
@littleadv, selbst die aktuellen Versionen von gcc und msvc sind voller Fehler, daher wäre ich mir nicht so sicher.
SK-logic,
3
Sie haben alle Warnungen aktiviert?
@ Thorbjørn Ravn Andersen: Ja, ich habe sie aktiviert.
Scharfzahn
3
FWIW: 1) Ich versuche, nichts Kniffliges zu tun, das den Compiler zum Durcheinander bringen könnte. 2) Der einzige Ort, an dem die Optimierungsflags eine Rolle spielen (aus Gründen der Geschwindigkeit), ist der Code, in dem der Programmzähler einen erheblichen Teil seiner Zeit verbringt. Sofern Sie keine engen CPU-Schleifen schreiben, verbringt der PC in vielen Anwendungen im Wesentlichen die gesamte Zeit in Bibliotheken oder E / A. In dieser Art von App helfen Ihnen die / O-Schalter überhaupt nicht.
Mike Dunlavey

Antworten:

19

Ich würde sagen, es ist eine sichere Wette, dass in den allermeisten Fällen Ihr Code und nicht der Compiler kaputt ist. Und selbst in Ausnahmefällen, in denen es sich um den Compiler handelt, verwenden Sie wahrscheinlich eine undurchsichtige Sprachfunktion auf ungewöhnliche Weise, auf die der jeweilige Compiler nicht vorbereitet ist. Mit anderen Worten, Sie könnten wahrscheinlich Ihren Code so ändern, dass er idiomatischer ist und die Schwachstelle des Compilers vermieden wird.

Wenn Sie nachweisen können , dass Sie einen Compiler-Fehler gefunden haben (basierend auf der Sprachspezifikation), melden Sie dies auf jeden Fall den Compiler-Entwicklern, damit sie es irgendwann reparieren können.

Péter Török
quelle
@ SK-Logik, fair genug, ich habe keine Statistiken, um es zu unterstützen. Es basiert auf meinen eigenen Erfahrungen und ich gebe zu, dass ich selten die Grenzen der Sprache und / oder des Compilers überschritten habe - andere tun dies möglicherweise öfter.
Péter Török
(1) @ SK-Logic: Habe gerade einen C ++ - Compiler-Fehler gefunden, den gleichen Code, der auf einem Compiler ausprobiert wurde und funktioniert, in einem anderen funktioniert er nicht mehr.
Umlcat
8
@umlcat: Höchstwahrscheinlich war es Ihr Code, abhängig von nicht angegebenem Verhalten. Auf einem Compiler entspricht es Ihren Erwartungen, auf einem anderen nicht. das heißt nicht, dass es kaputt ist.
Javier
@ Ritch Melton, haben Sie jemals LTO verwendet?
SK-logic
1
Ich stimme Crashworks zu, wenn es um Spielekonsolen geht. Es ist nicht ungewöhnlich, dass esoterische Compiler-Fehler in dieser speziellen Situation auftreten. Wenn Sie jedoch normale PCs mit einem stark genutzten Compiler anvisieren, ist es sehr unwahrscheinlich, dass Sie auf einen Compiler-Bug stoßen, den noch niemand zuvor gesehen hat.
Trevor Powell,
14

Genau wie bei allen anderen Bugs üblich: Führen Sie ein kontrolliertes Experiment durch. Grenzen Sie den verdächtigen Bereich ein, deaktivieren Sie die Optimierungen für alles andere und variieren Sie die Optimierungen, die auf diesen Codeabschnitt angewendet werden. Sobald Sie eine 100% ige Reproduzierbarkeit erreicht haben, können Sie Ihren Code variieren und Dinge einführen, die bestimmte Optimierungen beeinträchtigen könnten (z. B. mögliches Pointer-Aliasing, externe Aufrufe mit möglichen Nebenwirkungen usw.). Ein Blick auf den Assembler-Code in einem Debugger könnte ebenfalls hilfreich sein.

SK-Logik
quelle
könnte helfen mit was? Wenn es sich um einen Compiler-Fehler handelt, was dann?
Littleadv
2
@littleadv, wenn es sich um einen Compiler-Fehler handelt, können Sie entweder versuchen, ihn zu beheben (oder ihn nur richtig und ausführlich melden), oder Sie können herausfinden, wie Sie ihn in Zukunft vermeiden können, wenn Sie dazu verdammt sind, diesen Fehler weiterhin zu verwenden Version Ihres Compilers für eine Weile. Wenn es sich um etwas mit Ihrem eigenen Code handelt, eines der zahlreichen C ++ - Grenzprobleme, hilft diese Art der Überprüfung auch dabei, einen Fehler zu beheben und seine Art in Zukunft zu vermeiden.
SK-logic
Also, wie ich in meiner Antwort sagte - abgesehen von der Berichterstattung gibt es keinen großen Unterschied in der Behandlung, unabhängig davon, wessen Fehler es ist.
Littleadv
3
@littleadv, ohne die Natur eines Problems zu verstehen, werden Sie sich wahrscheinlich immer wieder damit auseinandersetzen. Und es gibt oft die Möglichkeit, einen Compiler selbst zu reparieren. Und ja, es ist leider nicht "unwahrscheinlich", dass ein Fehler in einem C ++ - Compiler gefunden wird.
SK-logic
10

Untersuchen Sie den resultierenden Assemblycode, und prüfen Sie, ob er den Anforderungen Ihrer Quelle entspricht. Denken Sie daran, dass die Chancen sehr hoch sind, dass wirklich Ihr Code in nicht offensichtlicher Weise schuld ist.

Loren Pechtel
quelle
1
Dies ist wirklich die einzige Antwort auf diese Frage. In diesem Fall besteht die Aufgabe des Compilers darin, Sie von C ++ in die Assemblersprache zu bringen. Sie denken, es ist der Compiler ... Überprüfen Sie die Arbeit des Compilers. So einfach ist das.
old_timer
7

In über 30 Jahren Programmierzeit habe ich immer noch nur etwa 10 echte Compiler-Fehler (Fehler bei der Codegenerierung) gefunden. Die Anzahl meiner eigenen (und der anderer) Fehler, die ich im selben Zeitraum gefunden und behoben habe, ist wahrscheinlich > 10.000. Meine "Faustregel" lautet dann, dass die Wahrscheinlichkeit, dass ein Fehler vom Compiler verursacht wird, <0,001 ist.

Paul R
quelle
1
Du hast Glück. Mein Durchschnitt liegt bei ungefähr einem wirklich schlimmen Bug pro Monat, und kleinere Borderline-Probleme treten viel häufiger auf. Und je höher die von Ihnen verwendete Optimierungsstufe ist, desto höher ist die Wahrscheinlichkeit von Compilerfehlern. Wenn Sie versuchen, -O3 und LTO zu verwenden, haben Sie das große Glück, nicht in kürzester Zeit ein paar davon zu finden. Und ich zähle hier nur die Fehler in den Release-Versionen - als Compiler-Entwickler habe ich bei meiner Arbeit viel mehr mit Problemen dieser Art zu tun, aber das zählt nicht. Ich weiß nur, wie einfach es ist, einen Compiler zu vermasseln.
SK-logic
2
25 Jahre und ich habe auch sehr viele gesehen. Die Compiler werden von Jahr zu Jahr schlechter.
old_timer
5

Ich fing an, einen Kommentar zu schreiben und entschied dann, dass es zu lang und zu viel ist, um es auf den Punkt zu bringen.

Ich würde argumentieren, dass es Ihr Code ist, der gebrochen ist. In dem unwahrscheinlichen Fall, dass Sie einen Fehler im Compiler entdeckt haben, sollten Sie ihn den Compiler-Entwicklern melden, aber hier endet der Unterschied.

Die Lösung besteht darin, das fehlerhafte Konstrukt zu identifizieren und neu zu faktorisieren, damit es dieselbe Logik auf unterschiedliche Weise ausführt. Das würde höchstwahrscheinlich das Problem lösen, egal ob der Fehler auf Ihrer Seite oder im Compiler liegt.

littleadv
quelle
5
  1. Lesen Sie Ihren Code noch einmal gründlich durch. Stellen Sie sicher, dass Sie keine Aktivitäten mit Nebenwirkungen in ASSERTs oder anderen Debug-Anweisungen (oder allgemeineren konfigurationsspezifischen Anweisungen) ausführen. Denken Sie auch daran, dass in einem Debugbuild der Speicher anders initialisiert wird - verräterische Zeigerwerte, die Sie hier überprüfen können: Debugging - Memory Allocation Representations . Wenn Sie in Visual Studio ausführen, verwenden Sie fast immer den Debug-Heap (auch im Release-Modus), es sei denn, Sie geben mit einer Umgebungsvariablen explizit an, dass dies nicht das ist, was Sie möchten.
  2. Überprüfen Sie Ihren Build. Es ist üblich, Probleme mit komplexen Builds an anderen Orten als dem eigentlichen Compiler zu bekommen - Abhängigkeiten sind oft der Schuldige. Ich weiß, dass "Haben Sie versucht, komplett neu zu erstellen" fast so ärgerlich ist wie "Haben Sie versucht, Windows neu zu installieren", aber es hilft oft. Versuchen Sie Folgendes: a) Neustarten. b) Löschen Sie alle Ihre Zwischen- und Ausgabedateien manuell und erstellen Sie sie neu.
  3. Durchsuchen Sie Ihren Code nach potenziellen Stellen, an denen Sie möglicherweise undefiniertes Verhalten auslösen. Wenn Sie eine Weile in C ++ gearbeitet haben, werden Sie wissen, dass es einige Stellen gibt, an denen Sie denken, dass "ich nicht VOLLSTÄNDIG sicher bin, dass ich das annehmen darf ..." - googeln Sie es oder fragen Sie hier nach Art des Codes, um zu sehen, ob es sich um undefiniertes Verhalten handelt oder nicht.
  4. Wenn dies immer noch nicht der Fall zu sein scheint, generieren Sie eine vorverarbeitete Ausgabe für die Datei, die die Probleme verursacht. Eine unerwartete Makroerweiterung kann jede Menge Spaß machen (ich erinnere mich an die Zeit, als ein Kollege entschied, dass ein Makro mit dem Namen H eine gute Idee wäre ...). Untersuchen Sie die vorverarbeitete Ausgabe auf unerwartete Änderungen zwischen Ihren Projektkonfigurationen.
  5. Letzten Ausweg - jetzt sind Sie wirklich im Land der Compiler-Fehler - sehen Sie sich die Assembly-Ausgabe an. Dies kann ein wenig Graben und Kämpfen erfordern, um einen Überblick über die eigentliche Arbeit der Versammlung zu bekommen, ist aber tatsächlich recht informativ. Sie können die Fähigkeiten, die Sie hier erwerben, auch zur Bewertung von Mikrooptimierungen verwenden, damit nicht alles verloren geht.
Joris Timmermans
quelle
+1 für "undefiniertes Verhalten" Ich bin von diesem gebissen worden. Schrieb einen Code, von int + intdem ein Überlauf abhing , genau so, als würde er zu einem Hardware-ADD-Befehl kompiliert. Es funktionierte einwandfrei, wenn es mit einer älteren Version von GCC kompiliert wurde, aber nicht, wenn es mit dem neueren Compiler kompiliert wurde. Anscheinend haben die netten Leute bei GCC entschieden, dass ihr Optimierer unter der Annahme arbeiten könnte, dass dies niemals der Fall ist, da das Ergebnis eines Ganzzahlüberlaufs undefiniert ist. Es hat einen wichtigen Zweig direkt aus dem Code heraus optimiert.
Solomon Slow
2

Wenn Sie wissen möchten, ob es sich um Ihren Code oder den Compiler handelt, müssen Sie die Spezifikation von C ++ genau kennen.

Wenn der Zweifel bestehen bleibt, müssen Sie die x86-Assembly genau kennen.

Wenn Sie nicht in der Stimmung sind, beides perfekt zu lernen, ist es mit ziemlicher Sicherheit ein undefiniertes Verhalten, das Ihr Compiler je nach Optimierungsstufe unterschiedlich auflöst.

mouviciel
quelle
(+1) @mouviciel: Es hängt auch davon ab, ob das Feature vom Compiler unterstützt wird, auch wenn es in der Spezifikation enthalten ist. Ich habe einen seltsamen Fehler mit GCC. Ich deklariere eine "einfache c-Struktur" mit einem "Funktionszeiger", der in der Spezifikation zulässig ist, aber in einigen Situationen funktioniert und in anderen nicht.
umlcat
1

Es ist wahrscheinlicher, dass ein Kompilierungsfehler im Standardcode oder ein interner Kompilierungsfehler auftritt, als dass Optimierer falsch liegen. Aber ich habe von Compilern gehört, die Schleifen falsch optimieren und dabei einige Nebenwirkungen, die eine Methode verursachen, vergessen.

Ich habe keine Vorschläge, wie man weiß, ob es Sie oder der Compiler ist. Sie können einen anderen Compiler versuchen.

Eines Tages fragte ich mich, ob es mein Code war oder nicht und jemand schlug mir Valgrind vor. Ich habe die 5 oder 10 Minuten damit verbracht, mein Programm damit auszuführen (glaube ich)valgrind --leak-check=yes myprog arg1 arg2 habe es getan, aber ich habe mit anderen Optionen gespielt) und es hat mir sofort EINE Zeile gezeigt, die unter einem bestimmten Fall ausgeführt wird, der das Problem war. Dann lief meine App reibungslos, ohne seltsame Abstürze, Fehler oder seltsames Verhalten. Valgrind oder ein anderes Tool wie es ist ein guter Weg, um zu wissen, ob es Ihr Code ist.

Randnotiz: Ich habe mich einmal gefragt, warum die Leistung meiner App nicht stimmt. Es stellte sich heraus, dass alle meine Leistungsprobleme auch in einer Zeile standen. Ich schrieb for(int i=0; i<strlen(sz); ++i) {. Die sz war ein paar mb. Aus irgendeinem Grund lief der Compiler auch nach der Optimierung jedes Mal länger. Eine Zeile kann eine große Sache sein. Von Auftritten bis zu Abstürzen


quelle
1

Immer häufiger kommt es vor, dass Compiler den Code für Dialekte von C unterbrechen, die Verhaltensweisen unterstützen, die nicht vom Standard vorgeschrieben sind, und es zulassen, dass Code, der auf diese Dialekte abzielt, effizienter ist, als es streng konformer Code sein könnte. In einem solchen Fall wäre es unfair, den Compiler, der einen Dialekt verarbeitet, der die erforderliche Semantik nicht unterstützt, als "fehlerhaften" Code zu bezeichnen, der für Compiler, die den Zieldialekt implementieren, 100% zuverlässig ist . Stattdessen rühren die Probleme einfach von der Tatsache her, dass die Sprache, die von modernen Compilern mit aktivierten Optimierungen verarbeitet wird, von Dialekten abweicht, die früher populär waren (und von vielen Compilern mit deaktivierten Optimierungen oder sogar mit aktivierten Optimierungen noch verarbeitet werden).

Beispielsweise wird viel Code für Dialekte geschrieben, die eine Reihe von Zeiger-Aliasing-Mustern als legitim erkennen, die nicht durch die gcc-Interpretation des Standards vorgeschrieben sind, und solche Muster verwenden, um eine einfache Übersetzung des Codes lesbarer und effizienter zu machen als es nach der Interpretation des C-Standards durch gcc möglich wäre. Solcher Code ist möglicherweise nicht mit gcc kompatibel, aber das bedeutet nicht, dass er kaputt ist. Es basiert einfach auf Erweiterungen, die gcc nur mit deaktivierten Optimierungen unterstützt.

Superkatze
quelle
Sicher ist, dass die Codierung der C-Standard-X + -Erweiterungen Y und Z nichts Falsches ist, solange dies Ihnen erhebliche Vorteile bringt, Sie wissen, dass Sie das getan haben, und Sie haben es gründlich dokumentiert. Leider sind normalerweise nicht alle drei Bedingungen erfüllt , und daher kann man mit Recht sagen, dass der Code fehlerhaft ist.
Deduplizierer
@Deduplicator: C89-Compiler wurden als aufwärtskompatibel mit ihren Vorgängern befördert, ebenso wie für C99 usw. Während C89 keine Anforderungen an Verhaltensweisen stellt, die zuvor auf einigen Plattformen, jedoch nicht auf anderen Plattformen definiert wurden, lässt die Aufwärtskompatibilität darauf schließen, dass C89-Compiler für Plattformen, die Verhaltensweisen als definiert angesehen hatten, sollten dies auch weiterhin tun. Die Begründung für die Beförderung kurzer Typen ohne Vorzeichen zu Zeichen lässt vermuten, dass die Autoren des Standards von den Compilern erwartet haben, dass sie sich so verhalten, unabhängig davon, ob der Standard dies vorschreibt oder nicht. Weiter ...
Supercat
Eine strenge Interpretation der Aliasing-Regeln würde die Aufwärtskompatibilität aus dem Fenster werfen und viele Arten von Code unbrauchbar machen, aber ein paar kleine Änderungen (z. B. das Identifizieren einiger Muster, bei denen typübergreifendes Aliasing erwartet und daher zulässig sein sollte) würden beide Probleme lösen . Der gesamte angegebene Zweck der Regel bestand darin, zu vermeiden, dass Compiler "pessimistische" Aliasing-Annahmen treffen müssen, aber "float x" angegeben werden, sollte die Annahme bestehen, dass "foo ((int *) & x)" x ändern könnte, selbst wenn "foo" dies nicht tut Schreiben Sie nicht auf Zeiger vom Typ "float *" oder "char *" als "pessimistisch" oder "offensichtlich"?
Supercat
0

Isolieren Sie die problematische Stelle und vergleichen Sie das beobachtete Verhalten mit dem, was gemäß der Sprachspezifikation geschehen soll. Auf jeden Fall nicht einfach, aber das müssen Sie tun, um zu wissen (und nicht nur anzunehmen ).

Ich wäre wahrscheinlich nicht so akribisch. Ich würde eher das Support-Forum / die Mailing-Liste des Compiler-Herstellers fragen. Wenn es sich wirklich um einen Fehler im Compiler handelt, können sie ihn möglicherweise beheben. Wahrscheinlich wäre es sowieso mein Code. Zum Beispiel können Sprachspezifikationen in Bezug auf die Speichersichtbarkeit beim Threading sehr unerklärlich sein und können nur bei Verwendung bestimmter Optimierungsflags auf einer bestimmten Hardware (!) Sichtbar werden. Einige Verhaltensweisen können von der Spezifikation undefiniert sein, so dass sie mit einigen Compilern / einigen Flags und mit anderen nicht funktionieren können.

Joonas Pulakka
quelle
0

Höchstwahrscheinlich weist Ihr Code ein undefiniertes Verhalten auf (wie andere erklärt haben, sind Fehler in Ihrem Code viel wahrscheinlicher als im Compiler, selbst wenn C ++ - Compiler so komplex sind, dass sie Fehler aufweisen; selbst die C ++ - Spezifikation weist Entwurfsfehler auf). . Und UB kann hier sein, auch wenn die kompilierte ausführbare Datei zufällig funktioniert (durch Pech).

Lesen Sie also Lattners Blog Was jeder C-Programmierer über undefiniertes Verhalten wissen sollte (das meiste davon gilt auch für C ++ 11).

Das Valgrind- Tool und die neuesten -fsanitize= Instrumentierungsoptionen für GCC (oder Clang / LLVM ) sollten ebenfalls hilfreich sein. Und natürlich alle Warnungen aktivieren:g++ -Wall -Wextra

Basile Starynkevitch
quelle