Einige hypermoderne C-Compiler schließen daraus, dass solche Eingaben niemals empfangen werden, wenn ein Programm bei bestimmten Eingaben Undefiniertes Verhalten aufruft. Folglich kann jeder Code, der irrelevant wäre, wenn solche Eingaben nicht empfangen würden, eliminiert werden.
Als einfaches Beispiel gegeben:
void foo(uint32_t);
uint32_t rotateleft(uint_t value, uint32_t amount)
{
return (value << amount) | (value >> (32-amount));
}
uint32_t blah(uint32_t x, uint32_t y)
{
if (y != 0) foo(y);
return rotateleft(x,y);
}
Ein Compiler kann daraus schließen, dass die Funktion niemals mit Null aufgerufen wird , da die Auswertung von value >> (32-amount)
undefiniertes Verhalten amount
ergibt, wenn Null ist. Der Aufruf an kann somit bedingungslos gemacht werden.blah
y
foo
Soweit ich das beurteilen kann, scheint sich diese Philosophie irgendwann im Jahr 2010 durchgesetzt zu haben. Die frühesten Beweise, die ich für ihre Wurzeln gesehen habe, stammen aus dem Jahr 2009 und sind im C11-Standard verankert, der ausdrücklich besagt, dass undefiniertes Verhalten überhaupt auftritt Punkt in der Ausführung eines Programms wird das Verhalten des gesamten Programms rückwirkend undefiniert.
War die Vorstellung, dass Compiler versuchen sollten, undefiniertes Verhalten zu verwenden, um umgekehrte kausale Optimierungen zu rechtfertigen (dh das undefinierte Verhalten in der rotateleft
Funktion sollte den Compiler veranlassen, anzunehmen, blah
dass es mit einer Nicht-Null aufgerufen worden sein muss y
, unabhängig davon, ob irgendetwas jemals dazu führen würde oder nichty
zu einen Wert ungleich Null halten), der vor 2009 ernsthaft befürwortet wurde? Wann wurde so etwas zum ersten Mal ernsthaft als Optimierungstechnik vorgeschlagen?
[Nachtrag]
Einige Compiler haben sogar im 20. Jahrhundert Optionen aufgenommen, um bestimmte Arten von Rückschlüssen auf Schleifen und die darin berechneten Werte zu ermöglichen. Zum Beispiel gegeben
int i; int total=0;
for (i=n; i>=0; i--)
{
doSomething();
total += i*1000;
}
Ein Compiler kann es auch ohne die optionalen Schlussfolgerungen wie folgt umschreiben:
int i; int total=0; int x1000;
for (i=n, x1000=n*1000; i>0; i--, x1000-=1000)
{
doSomething();
total += x1000;
}
da das Verhalten dieses Codes genau mit dem Original übereinstimmen würde, selbst wenn der Compiler spezifizierte, dass int
Werte immer in mod-65536 Zwei-Komplement-Weise umbrochen werden . Die zusätzliche-Inferenz - Option lassen würde der Compiler erkennen , dass da i
und x1000
Null zugleich überqueren sollte, kann die ehemalige Variable eliminiert werden:
int total=0; int x1000;
for (x1000=n*1000; x1000 > 0; x1000-=1000)
{
doSomething();
total += x1000;
}
Auf einem System, auf dem int
Werte in Mod 65536 eingeschlossen sind, würde ein Versuch, eine der ersten beiden Schleifen mit n
33 auszuführen , dazu führen, doSomething()
dass 33 Mal aufgerufen wird. Im Gegensatz dazu würde die letzte Schleife überhaupt nicht aufrufen doSomething()
, obwohl der erste Aufruf von tatsächlich von Vorteil wäre. Darüber hinaus entschuldigte sich die Compiler-Dokumentation in der Regel dafür, dass sie das Verhalten von Programmen ändern würde - selbst von Programmen, die sich mit UB befassten.doSomething()
einem arithmetischen Überlauf vorausgegangen wäre. Ein solches Verhalten könnte als "nicht kausal" angesehen werden, aber die Auswirkungen sind ziemlich gut eingeschränkt, und es gibt viele Fälle, in denen das Verhalten nachweislich harmlos wäre (in Fällen, in denen eine Funktion erforderlich ist, um einen Wert zu liefern, wenn eine Eingabe gegeben wird, aber der Wert kann beliebig sein, wenn die Eingabe ungültig ist, und die Schleife wird schneller beendet, wenn ein ungültiger Wert von angegeben wirdn
Ich bin daran interessiert, wann sich die Einstellungen von Compiler-Autoren von der Idee abwandten, dass Plattformen, wenn dies praktikabel ist, einige verwendbare Verhaltensbeschränkungen auch in Fällen dokumentieren sollten, die nicht vom Standard vorgeschrieben sind, zu der Idee, dass Konstrukte, die auf Verhaltensweisen beruhen, die nicht von der Norm vorgeschrieben sind Standard sollte als unzulässig eingestuft werden, selbst wenn er auf den meisten vorhandenen Compilern genauso gut oder besser funktioniert als jeder streng konforme Code, der dieselben Anforderungen erfüllt (häufig sind Optimierungen möglich, die bei streng konformem Code nicht möglich wären).
quelle
shape->Is2D()
, dass ein Objekt aufgerufen wurde, das nicht abgeleitet wurde vonShape2D
. Es gibt einen großen Unterschied zwischen der Optimierung von Code, der nur relevant wäre, wenn bereits ein kritisches undefiniertes VerhaltenShape2D::Is2D
ist eigentlich besser , immer zu springen, als das Programm es verdient.int prod(int x, int y) {return x*y;}
hätte ausgereicht. Die strikte Einhaltung von" Nukes nicht starten "würde jedoch Code erfordern, der schwerer zu lesen ist und fast sicherlich viel langsamer auf vielen Plattformen laufen.Antworten:
Undefiniertes Verhalten wird in Situationen verwendet, in denen es für die Spezifikation nicht möglich ist, das Verhalten anzugeben, und es wurde immer geschrieben, um absolut jedes mögliche Verhalten zuzulassen.
Die extrem lockeren Regeln für UB sind hilfreich, wenn Sie darüber nachdenken, was ein spezifikationskonformer Compiler durchmachen muss. Möglicherweise verfügen Sie über genügend Kompilierungsleistung, um einen Fehler auszugeben, wenn Sie in einem Fall eine schlechte UB ausführen. Fügen Sie jedoch einige Rekursionsebenen hinzu, und das Beste, was Sie jetzt tun können, ist eine Warnung. Die Spezifikation hat kein Konzept von "Warnungen". Wenn die Spezifikation also ein Verhalten angegeben hätte, müsste es "ein Fehler" sein.
Der Grund, warum wir immer mehr Nebenwirkungen sehen, ist der Drang zur Optimierung. Das Schreiben eines spezifikationskonformen Optimierers ist schwierig. Es ist brutal, einen spezifikationskonformen Optimierer zu schreiben, der auch bemerkenswert gute Arbeit leistet, um zu erraten, was Sie beabsichtigt haben, als Sie außerhalb der Spezifikation waren. Für die Compiler ist es viel einfacher, wenn sie annehmen, dass UB UB bedeutet.
Dies gilt insbesondere für gcc, das versucht, viele, viele Befehlssätze mit demselben Compiler zu unterstützen. Es ist weitaus einfacher, UB UB-Verhalten hervorbringen zu lassen, als zu versuchen, sich mit allen Möglichkeiten auseinanderzusetzen, mit denen jeder einzelne UB-Code auf jeder Plattform schief gehen kann, und dies in die frühen Sätze des Optimierers einzubeziehen.
quelle
x-y > z
willkürlich 0 oder 1x-y
ergibt, wenn dies nicht als "int" dargestellt werden kann, bietet eine solche Plattform mehr Optimierungsmöglichkeiten als eine Plattform, bei der der Ausdruck entwederUINT_MAX/2+1+x+y > UINT_MAX/2+1+z
oder geschrieben werden muss(long long)x+y > z
."Undefiniertes Verhalten kann dazu führen, dass der Compiler Code neu schreibt" ist in Schleifenoptimierungen seit langem aufgetreten.
Nehmen Sie eine Schleife (a und b sind zum Beispiel Zeiger auf double)
Wir erhöhen ein int, kopieren ein Array-Element und vergleichen es mit einem Limit. Ein optimierender Compiler entfernt zuerst die Indizierung:
Wir entfernen den Fall n <= 0:
Jetzt eliminieren wir die Variable i:
Wenn nun n = 2 ^ 29 auf einem 32-Bit-System oder 2 ^ 61 auf einem 64-Bit-System ist, haben wir bei typischen Implementierungen das Limit tmp1 == und es wird kein Code ausgeführt. Ersetzen Sie nun die Zuweisung durch etwas, das lange dauert, damit der ursprüngliche Code niemals in den unvermeidlichen Absturz gerät, weil er zu lange dauert und der Compiler den Code geändert hat.
quelle
volatile
Zeiger neu zun
sequenzieren. Daher ist das Verhalten in dem Fall, in dem Zeiger umbrochen werden, gleichbedeutend damit, dass ein Speicher außerhalb der Grenzen einen temporären Speicherorti
vor allem anderen enthält das passiert. Wenna
oderb
waren flüchtig, dokumentierte die Plattform, dass flüchtige Zugriffe physische Lade- / Speicheroperationen in der angeforderten Reihenfolge erzeugen, und die Plattform definiert alle Mittel, über die solche Anforderungen ...i
sei denn, sie wurden ebenfalls flüchtig gemacht). Das wäre jedoch ein ziemlich seltener Fall von Verhaltensecken. Wenna
undb
nicht flüchtig sind, würde ich vorschlagen, dass es keine plausible beabsichtigte Bedeutung für das gibt, was der Code tun soll, wenn ern
so groß ist, dass der gesamte Speicher überschrieben wird. Im Gegensatz dazu haben viele andere Formen von UB plausible beabsichtigte Bedeutungen.if (x-y>z) do_something()
`Es ist egal, ob erdo_something
im Falle eines Überlaufs ausgeführt wird, vorausgesetzt, der Überlauf hat keine andere Wirkung. Gibt es eine Möglichkeit, das obendo_something
)? Selbst wenn es Schleifenoptimierungen verboten wäre, ein Verhalten zu erzielen, das nicht mit einem losen Überlaufmodell vereinbar ist, könnten Programmierer Code so schreiben, dass Compiler optimalen Code generieren können. Gibt es eine Möglichkeit, Ineffizienzen zu umgehen, die durch ein Modell "Überlauf um jeden Preis vermeiden" erzwungen werden?In C und C ++ war es immer so, dass aufgrund undefinierten Verhaltens alles passieren kann. Daher war es auch immer so, dass ein Compiler davon ausgehen kann, dass Ihr Code kein undefiniertes Verhalten aufruft: Entweder enthält Ihr Code kein undefiniertes Verhalten, dann war die Annahme richtig. Oder es gibt ein undefiniertes Verhalten in Ihrem Code. Was auch immer aufgrund der falschen Annahme passiert, wird von " irgendetwas " abgedeckt kann passieren" .
Wenn Sie sich die Funktion "Einschränken" in C ansehen, besteht der springende Punkt der Funktion darin, dass der Compiler davon ausgehen kann, dass es kein undefiniertes Verhalten gibt. Daher haben wir den Punkt erreicht, an dem der Compiler nicht nur kann, sondern sollte annehmen annehmen , dass es kein undefiniertes gibt Verhalten.
In dem von Ihnen angegebenen Beispiel werden die Assembler-Anweisungen, die normalerweise auf x86-basierten Computern zum Implementieren der Links- oder Rechtsverschiebung verwendet werden, um 0 Bit verschoben, wenn die Anzahl der Verschiebungen 32 für 32-Bit-Code oder 64 für 64-Bit-Code beträgt. Dies führt in den meisten praktischen Fällen zu unerwünschten Ergebnissen (und Ergebnissen, die nicht mit ARM oder PowerPC identisch sind), sodass der Compiler zu Recht davon ausgehen kann, dass ein solches undefiniertes Verhalten nicht auftritt. Sie können Ihren Code in ändern
und schlagen Sie den gcc- oder Clang-Entwicklern vor, dass auf den meisten Prozessoren der Code "amount == 0" vom Compiler entfernt werden sollte, da der für den Shift-Code generierte Assembler-Code das gleiche Ergebnis wie value liefert, wenn amount == 0 ist.
quelle
x>>y
[für vorzeichenlosex
], die funktionieren würde, wenn eine Variabley
einen Wert von 0 bis 31 enthält und etwas anderes als 0 oderx>>(y & 31)
für andere Werte ausführt, so effizient sein könnte wie eine, die etwas anderes ausführt ;; Ich kenne keine Plattform, auf der die Garantie, dass keine andere als eine der oben genannten Maßnahmen ergriffen würde, erhebliche Kosten verursachen würde. Die Idee, dass Programmierer eine kompliziertere Formulierung in Code verwenden sollten, die niemals auf obskuren Maschinen ausgeführt werden müsste, wäre als absurd angesehen worden.x
oder0
auf einigen dunklen Plattformen abfangen "zu"x>>32
könnte den Compiler veranlassen, die Bedeutung von anderem Code neu zu schreiben "verschoben haben. Die frühesten Beweise, die ich finden kann, stammen aus dem Jahr 2009, aber ich bin gespannt, ob frühere Beweise vorliegen.0<=amount && amount<32
. Ob größere / kleinere Werte sinnvoll sind? Ich dachte, ob sie es tun, ist Teil der Frage. Und angesichts von Bit-Ops keine Klammern zu verwenden, ist wahrscheinlich eine schlechte Idee, sicher, aber sicherlich kein Fehler.(y mod 32)
für 32-Bitx
und(y mod 64)
für 64-Bit implementierenx
. Beachten Sie, dass es relativ einfach ist, Code auszugeben, der über alle CPU-Architekturen hinweg ein einheitliches Verhalten erzielt - durch Maskieren des Verschiebungsbetrags. Dies erfordert normalerweise eine zusätzliche Anweisung. Aber leider ...Dies liegt daran, dass Ihr Code einen Fehler enthält:
Mit anderen Worten, es überspringt nur dann die Kausalitätsbarriere, wenn der Compiler feststellt, dass Sie bei bestimmten Eingaben zweifelsfrei undefiniertes Verhalten aufrufen .
Indem Sie unmittelbar vor dem Aufruf von undefiniertem Verhalten zurückkehren, teilen Sie dem Compiler mit, dass Sie bewusst verhindern, dass dieses undefinierte Verhalten ausgeführt wird, und der Compiler erkennt dies an.
Mit anderen Worten, wenn Sie einen Compiler haben, der versucht, die Spezifikation auf sehr strenge Weise durchzusetzen, müssen Sie jede mögliche Argumentvalidierung in Ihrem Code implementieren. Darüber hinaus muss diese Validierung vor dem Aufruf des undefinierten Verhaltens erfolgen.
Warten! Und es gibt noch mehr!
Wenn Compiler diese super verrückten, aber super logischen Dinge tun, müssen Sie dem Compiler unbedingt mitteilen, dass eine Funktion die Ausführung nicht fortsetzen soll. Somit wird das
noreturn
Schlüsselwort für diefoo()
Funktion jetzt obligatorisch .quelle