Die gcc-Optimierung überspringt die Initialisierung des zugewiesenen Speichers

8

Mit gcc 4.9.2 20150304 64 Bit stieß ich auf dieses scheinbar seltsame Verhalten:

%Vor%

Im Code ordne ich einen double auf dem Heap zu, initialisiere ihn und gebe dann ein weiteres double initialisiert mit der Adresse des ersten in ein intptr_t umgewandelt zurück. Dies erzeugt mit der Optimierung -O2 im 32-Bit-Modus den folgenden Assembly-Code:

%Vor%

und erstaunlicherweise ist die Initialisierung des zugewiesenen double komplett weg.

Beim Generieren von Code mit -O0 funktioniert alles wie erwartet und der entsprechende Code ist stattdessen:

%Vor%

Frage

Habe ich irgendetwas Ungültiges getan (ich denke speziell an Aliasing-Regeln, auch wenn es mir scheint, dass das Überspringen der Initialisierung keine Rechtfertigung hat) oder ist das nur ein GCC-Bug?

Beachten Sie, dass das gleiche Problem beim Kompilieren auf 64-Bit-Code auftritt (formal intptr_t im 64-Bit-Modus ist 8 Bytes und daher kann ad double es nicht genau darstellen ... das tut das nicht t passiert aber, weil auf x86-64 nur 48 der 64 Bits von Adressen verwendet werden und ein double kann genau diese Werte darstellen).

    
6502 09.05.2015, 08:01
quelle

5 Antworten

1

Es scheint ein Bug ... sogar mit dem vereinfachten Code

%Vor%

bei der Kompilierung mit -O2 initialisiert den Speicher nicht.

Der Code funktioniert korrekt und gibt direkt intptr_t (oder unsigned long long ) zurück; aber es nach der Konvertierung in ein double zurückgeben funktioniert nicht, da gcc anscheinend davon ausgeht, dass Sie in diesem Fall nicht mehr auf den Speicher zugreifen können.

Dies ist im 32-Bit-Modus eindeutig falsch (wobei intptr_t 4 Byte und double 53 Bits Genauigkeit für Ganzzahlen angibt), aber auch für den 64-Bit-Modus, wobei uintptr_t tatsächlich 8 Byte verwendete Werte sind sind 48 Bits).

BEARBEITEN

Ich bin mir nicht sicher, aber das Problem könnte mit der "toten Code-Eliminierung auf Baum" ( -ftree-dce ) zusammenhängen. Beim Kompilieren im 32-Bit-Modus, der Optimierungen -O2 aktiviert, aber dieses spezifische mit -fno-tree-dce deaktiviert, ändert sich die Programmausgabe und ist korrekt, aber der generierte Code ist nicht .

Genauer gesagt enthält die nicht-inlinierte Version von doit keinen Initialisierungscode, aber der in main erzeugte Code inlinert den Aufruf und der Optimierer "weiß", dass der Wert des Speichers 3.14 ist und druckt diesen direkt in Ausgabe.

BEARBEITEN 2

Bestätigt als Fehler, bereits im Stamm behoben.

Workaround bis zur nächsten Veröffentlichung ist -fno-tree-pta

    
6502 09.05.2015, 09:25
quelle
4

Die Optimierung erlaubt es, Code im Falle von UB zu entfernen, aber hier sollte es nicht.

Sie haben eine unnötige Umwandlung in Value *ptr = (Value *)malloc(sizeof(Value)); , aber das sollte harmlos sein.

Diese Zeile res.d = (unsigned long long) ptr; sollte besser res.d = (intptr_t) ptr; sein, weil intptr_t explizit Zeiger empfangen darf, und Sie können dann einen ganzzahligen Wert in einer double -Variable setzen: Sie können die Genauigkeit verlieren, sollten es aber nicht sei UB.

Ich kann es nicht testen (weil ich kein gcc 4.9 habe), aber wenn du das gleiche Problem hast:

%Vor%

Ich würde auf einen gcc-Fehler schließen.

Ich könnte versuchen, die vereinfachte Version des Codes mit clang Version 3.4.1 unter FreeBSD 10.1 zu kompilieren

cc -O3 -S doit.c gibt (zum Codeteil hinuntergestreift):

%Vor%

Es ist nicht die gleiche Kompilierung wie gcc, aber clang macht die 3.14 Initialisierung sogar bei -O3 Optimierungsebene (dump hex für 3.14 ist 0x40091eb851eb851f )

Nach dem Lesen anderer Kommentare und Antworten denke ich, dass die eigentliche Ursache des Problems darin liegt, dass gcc die Zwischenbesetzung überspringt und return (double)((uintptr_t) ptr); als return (double) ptr; liest - na ja, nicht genau, weil es dann so wäre Sei ein Syntaxfehler, aber es gibt immer noch UB, da am Ende ein Zeigerwert in eine doppelte Variable endet. Aber wenn wir die Zeile mit der Zwischenform zerlegen, sollte sie (IMHO) wie folgt gelesen werden:

%Vor%     
Serge Ballesta 09.05.2015 09:06
quelle
2

Ich sehe hier nichts Seltsames. Sie lesen nie das von Ihnen geschriebene 7 , sondern schreiben das Ergebnis von malloc in ein double :

%Vor%

Wenn Sie einen Zeiger in ein double konvertieren, verlieren Sie höchstwahrscheinlich eine Genauigkeit und machen den Speicher unzugänglich (UB).

Wenn Sie jedoch den Zeiger auf eine ganze Zahl speichern ( .u ), behandelt GCC dies als Alias-Speicher und behält die Initialisierung bei:

%Vor%

kompiliert zu

%Vor%

Das Problem besteht also darin, dass Sie den Zeiger auf einen Doppelpunkt speichern.

BTW, (double)ptr ist ein Kompilierfehler, wie es Standard erfordert:

  

6.5.4 Cast-Operatoren

     

[...]

     

4 Ein Zeigertyp darf nicht in einen Gleitkommatyp umgewandelt werden. Ein Floating-Typ darf nicht in einen beliebigen Zeigertyp konvertiert werden.

Ab N1548 Entwurf

    
myaut 09.05.2015 09:42
quelle
0

C ist kein Assembler. C kann ein undefiniertes Verhalten auslösen, wenn jemand, der es als Assembler auf hoher Ebene ansieht, nicht sehen kann, warum. Als ein Beispiel: Bei zwei Arrays int a [10] und int b [10] ist es möglich, dass durch Zufall & amp; a [10] == & amp; b [0]. Jedoch der folgende Code

%Vor%

ruft undefiniertes Verhalten auf, wenn p == & amp; b [0]. Zwei Zeiger, p und & amp; b [0], vergleichen sich gleich und bestehen aus den gleichen Bits, verhalten sich aber unterschiedlich. (Wenn Sie nicht zustimmen, schauen Sie sich "restricte" Zeiger an, wo der ganze Punkt ist, dass sich Zeiger, die sich mit Gleichem vergleichen, anders verhalten können).

Die Regeln für die Konvertierung zu uintptr_t lauten wie folgt: Jeder gültige Zeiger kann in uintptr_t konvertiert werden, und das Ergebnis kann zurück in einen Zeiger konvertiert werden, der den gleichen Zeiger enthält. Die Werte sind implementierungsdefiniert, außer dass das Konvertieren eines Nullzeigers in uintptr_t eine Null ergeben muss und das Konvertieren einer 0 in einen Zeiger einen Nullzeiger ergeben muss. Nichts erfordert, dass die Konvertierung einfach sein sollte, oder sollte sein, was Sie denken, dass es sein sollte.

Die Konvertierung in uintptr_t ist implementiert. Wenn Zeiger in einer Architektur auf n & lt; = 62 Bit beschränkt sind, dann ist es durchaus möglich, dass die Umwandlung wie folgt lautet: Wenn p ein Null-Zeiger ist, wandle es in Null um. Wenn p kein Nullzeiger ist, nimm die n Bits, verschiebe sie um (63 - n) Bits nach links, oder das Ergebnis mit 0x8000 0000 0000 0001. Das Ergebnis ist garantiert nicht ohne Verlust in das Doppelte konvertierbar. Wenn der Befehl uintptr_t in double konvertiert wird, ist das Ergebnis so, dass er nicht mehr in einen gültigen Zeiger umgewandelt werden kann.

Wenn also (double) (uintptr_t) p der einzige von p abgeleitete Wert ist, dann kann p nicht rekonstruiert werden, der Zeiger p geht verloren und eine Zuweisung zu * p kann wegoptimiert werden, da * p nicht sein kann Lies erneut.

    
gnasher729 09.05.2015 23:16
quelle
0

Ich glaube, gcc hat Recht. Sie haben den Wert nicht verwendet und keinen Zeiger darauf zurückgegeben. Daher wird angenommen, dass der Wert nicht mehr erreichbar ist.

Sie hätten den Zeiger zurückgeben und an anderer Stelle doppelklicken müssen, oder Sie benötigen eine Vereinigung, damit gcc weiß, dass der Zeiger auf den Wert noch existiert:

%Vor%     
hdante 11.05.2015 10:38
quelle

Tags und Links