Dies ist ein Clang-Fehler
... beim Inlinen einer Funktion mit einer Endlosschleife. Das Verhalten ist anders, wenn es while(1);
direkt in main erscheint, was für mich sehr fehlerhaft riecht.
Siehe @ Arnavion Antwort für eine Zusammenfassung und Links. Der Rest dieser Antwort wurde geschrieben, bevor ich die Bestätigung erhielt, dass es sich um einen Fehler handelte, geschweige denn um einen bekannten Fehler.
Um die Titelfrage zu beantworten: Wie erstelle ich eine Endlosschleife, die nicht optimiert werden kann? ? -
Erstellen Sie die()
ein Makro und keine Funktion , um diesen Fehler in Clang 3.9 und höher zu umgehen. (Frühere Clang-Versionen behalten entweder die Schleife bei oder geben einecall
an eine Nicht-Inline-Version der Funktion mit der Endlosschleife aus.) Dies scheint sicher zu sein, selbst wenn die print;while(1);print;
Funktion in ihren Aufrufer ( Godbolt ) eingebunden ist . -std=gnu11
vs. -std=gnu99
ändert nichts.
Wenn Sie sich nur für GNU C interessieren__asm__("");
, funktioniert auch P__J __ in der Schleife und sollte die Optimierung des umgebenden Codes für Compiler, die ihn verstehen , nicht beeinträchtigen . GNU C Basic asm-Anweisungen sind implizitvolatile
, daher gilt dies als sichtbarer Nebeneffekt, der so oft "ausgeführt" werden muss wie in der abstrakten C-Maschine. (Und ja, Clang implementiert den GNU-Dialekt von C, wie im GCC-Handbuch dokumentiert.)
Einige Leute haben argumentiert, dass es legal sein könnte, eine leere Endlosschleife zu optimieren. Ich bin nicht einverstanden 1 , aber selbst wenn wir das akzeptieren, kann es nicht auch legal sein für Clang Aussagen nach der Schleife nicht erreichbar sind , anzunehmen, und die Ausführung fällt vom Ende der Funktion in die nächste Funktion oder in Müll lassen das dekodiert als zufällige Anweisungen.
(Das wäre standardkonform für Clang ++ (aber immer noch nicht sehr nützlich); Endlosschleifen ohne Nebenwirkungen sind UB in C ++, aber nicht C.
Is while (1); undefiniertes Verhalten in C? UB lässt den Compiler grundsätzlich alles ausgeben Für Code auf einem Ausführungspfad, der definitiv auf UB trifft. Eine asm
Anweisung in der Schleife würde diese UB für C ++ vermeiden. In der Praxis entfernt Clang-Kompilierung als C ++ jedoch keine unendlichen leeren Schleifen mit konstantem Ausdruck, außer beim Inlining, genau wie bei Kompilieren als C.)
Durch manuelles Inlining wird while(1);
geändert, wie Clang es kompiliert: Endlosschleife in asm vorhanden. Dies ist, was wir von einem POV für Regeljuristen erwarten würden.
#include <stdio.h>
int main() {
printf("begin\n");
while(1);
//infloop_nonconst(1);
//infloop();
printf("unreachable\n");
}
Auf dem Godbolt-Compiler-Explorer wird Clang 9.0 -O3 als C ( -xc
) für x86-64 kompiliert :
main: # @main
push rax # re-align the stack by 16
mov edi, offset .Lstr # non-PIE executable can use 32-bit absolute addresses
call puts
.LBB3_1: # =>This Inner Loop Header: Depth=1
jmp .LBB3_1 # infinite loop
.section .rodata
...
.Lstr:
.asciz "begin"
Derselbe Compiler mit denselben Optionen kompiliert einen main
, infloop() { while(1); }
der zuerst denselben aufruft puts
, dann aber main
nach diesem Punkt keine Anweisungen mehr ausgibt. Wie gesagt, die Ausführung fällt einfach vom Ende der Funktion in die nächste Funktion (aber der Stapel ist für die Funktionseingabe falsch ausgerichtet, sodass es sich nicht einmal um einen gültigen Tailcall handelt).
Die gültigen Optionen wären zu
- eine
label: jmp label
Endlosschleife ausgeben
- oder (wenn wir akzeptieren, dass die Endlosschleife entfernt werden kann) einen weiteren Aufruf ausgeben, um die 2. Zeichenfolge zu drucken, und dann
return 0
von main
.
Ein Absturz oder eine andere Fortsetzung ohne Drucken von "nicht erreichbar" ist für eine C11-Implementierung eindeutig nicht in Ordnung, es sei denn, es gibt UB, die ich nicht bemerkt habe.
Fußnote 1:
Für die Aufzeichnung stimme ich der Antwort von @ Lundin zu, die den Standard für den Beweis zitiert , dass C11 die Annahme einer Beendigung für Endlosschleifen mit konstantem Ausdruck nicht zulässt, selbst wenn sie leer sind (keine E / A, flüchtig, Synchronisation oder andere) sichtbare Nebenwirkungen).
Dies ist der Satz von Bedingungen, unter denen eine Schleife zu einer leeren ASM-Schleife für eine normale CPU kompiliert werden kann. (Selbst wenn der Body in der Quelle nicht leer war, können Zuweisungen zu Variablen für andere Threads oder Signalhandler ohne Data-Race-UB nicht sichtbar sein, während die Schleife ausgeführt wird. Eine konforme Implementierung könnte solche Schleifenkörper also entfernen, wenn dies gewünscht wird Dann bleibt die Frage, ob die Schleife selbst entfernt werden kann. ISO C11 sagt ausdrücklich nein.)
Angesichts der Tatsache, dass C11 diesen Fall als einen Fall auszeichnet, bei dem die Implementierung nicht davon ausgehen kann, dass die Schleife endet (und dass es sich nicht um UB handelt), scheint es klar zu sein, dass die Schleife zur Laufzeit vorhanden sein soll. Eine Implementierung, die auf CPUs mit einem Ausführungsmodell abzielt, das nicht unendlich viel Arbeit in endlicher Zeit erledigen kann, hat keine Rechtfertigung für das Entfernen einer leeren konstanten Endlosschleife. Oder sogar im Allgemeinen geht es bei der genauen Formulierung darum, ob davon ausgegangen werden kann, dass sie "enden" oder nicht. Wenn eine Schleife nicht beendet werden kann, bedeutet dies, dass späterer Code nicht erreichbar ist, unabhängig davon, welche Argumente Sie zu Mathematik und Unendlichkeiten vorbringen und wie lange es dauert, auf einer hypothetischen Maschine unendlich viel Arbeit zu leisten.
Darüber hinaus ist Clang nicht nur eine ISO C-konforme DeathStation 9000, sondern soll auch für die reale Low-Level-Systemprogrammierung, einschließlich Kernel und eingebetteter Inhalte, nützlich sein. Unabhängig davon, ob Sie Argumente über C11 akzeptieren, die das Entfernen von C11 ermöglichenwhile(1);
, ist es nicht sinnvoll, dass Clang dies tatsächlich tun möchte. Wenn Sie schreiben while(1);
, war das wahrscheinlich kein Unfall. Das Entfernen von Schleifen, die versehentlich unendlich werden (mit Laufzeitvariablen-Steuerausdrücken), kann nützlich sein, und es ist für Compiler sinnvoll, dies zu tun.
Es ist selten, dass Sie nur bis zum nächsten Interrupt drehen möchten, aber wenn Sie das in C schreiben, ist das definitiv das, was Sie erwarten. (Und was ist geschehen in GCC und Clang, mit Ausnahme von Clang , wenn die Endlos - Schleife in einer Wrapper - Funktion ist).
Wenn der Scheduler in einem primitiven Betriebssystemkernel beispielsweise keine auszuführenden Aufgaben hat, wird möglicherweise die inaktive Aufgabe ausgeführt. Eine erste Implementierung davon könnte sein while(1);
.
Oder für Hardware ohne stromsparende Leerlauffunktion ist dies möglicherweise die einzige Implementierung. (Bis in die frühen 2000er Jahre war das auf x86 meiner Meinung nach nicht selten. Obwohl der hlt
Befehl vorhanden war, konnte IDK eine bedeutende Menge Strom sparen, bis CPUs anfingen, Leerlaufzustände mit geringem Stromverbrauch zu haben.)