Questo è un bug di Clang
... quando si allinea una funzione contenente un ciclo infinito. Il comportamento è diverso quando while(1);
appare direttamente nella parte principale, che ha un odore molto difettoso per me.
Vedi la risposta di @ Arnavion per un riepilogo e collegamenti. Il resto di questa risposta è stato scritto prima che avessi la conferma che si trattava di un bug, per non parlare di un bug noto.
Per rispondere alla domanda del titolo: Come faccio a creare un ciclo vuoto infinito che non verrà ottimizzato? ? -
crea die()
una macro, non una funzione , per aggirare questo bug in Clang 3.9 e versioni successive. (Le versioni precedenti di Clang mantengono il ciclo o emettono unacall
versione non in linea della funzione con il ciclo infinito.) Ciò sembra essere sicuro anche se la print;while(1);print;
funzione si allinea nel suo chiamante ( Godbolt ). -std=gnu11
vs. -std=gnu99
non cambia nulla.
Se ti preoccupi solo di GNU C, P__J ____asm__("");
all'interno del ciclo funziona anche e non dovrebbe danneggiare l'ottimizzazione di alcun codice circostante per i compilatori che lo capiscono. Le istruzioni asm di GNU C Basic sono implicitevolatile
, quindi questo conta come un effetto collaterale visibile che deve "eseguire" tante volte quante sarebbe nella macchina astratta C. (E sì, Clang implementa il dialetto GNU di C, come documentato dal manuale GCC.)
Alcune persone hanno sostenuto che potrebbe essere legale ottimizzare un ciclo infinito vuoto. Non sono d'accordo 1 , ma anche se lo accettiamo, non può anche essere legale per Clang assumere affermazioni dopo che il ciclo è irraggiungibile e lasciare che l'esecuzione cada dalla fine della funzione nella funzione successiva o in immondizia che decodifica come istruzioni casuali.
(Sarebbe conforme agli standard per Clang ++ (ma ancora non molto utile); loop infiniti senza effetti collaterali sono UB in C ++, ma non C.
Is while (1); comportamento indefinito in C? UB consente al compilatore di emettere praticamente qualsiasi cosa per il codice su un percorso di esecuzione che incontrerà sicuramente UB. Un'istruzione asm
nel ciclo eviterebbe questo UB per C ++. Ma in pratica, la compilazione di Clang come C ++ non rimuove i cicli vuoti infiniti ad espressione costante, tranne quando in linea, come quando compilando come C.)
L'allineamento manuale while(1);
modifica il modo in cui Clang lo compila: loop infinito presente in asm. Questo è quello che ci aspetteremmo da un avvocato delle regole POV.
#include <stdio.h>
int main() {
printf("begin\n");
while(1);
//infloop_nonconst(1);
//infloop();
printf("unreachable\n");
}
Nell'esploratore del compilatore Godbolt , Clang 9.0 -O3 compilando come C ( -xc
) per x86-64:
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"
Lo stesso compilatore con le stesse opzioni compila prima uno main
che chiama infloop() { while(1); }
lo stesso puts
, ma poi smette di emettere istruzioni main
dopo quel punto. Quindi, come ho detto, l'esecuzione cade appena alla fine della funzione, in qualunque funzione sia successiva (ma con lo stack non allineato per l'inserimento della funzione, quindi non è nemmeno un richiamo valido).
Le opzioni valide sarebbero
- emette un
label: jmp label
ciclo infinito
- oppure (se accettiamo che il ciclo infinito può essere rimosso) emette un'altra chiamata per stampare la 2a stringa e quindi
return 0
da main
.
Arresto anomalo o continuare senza stampare "irraggiungibile" chiaramente non è corretto per un'implementazione C11, a meno che non ci sia UB che non ho notato.
Nota 1:
Per la cronaca, sono d'accordo con la risposta di @ Lundin che cita lo standard per l'evidenza che C11 non consente l'assunzione di terminazione per loop infiniti ad espressione costante, anche quando sono vuoti (nessun I / O, volatile, sincronizzazione o altro effetti collaterali visibili).
Questo è l'insieme di condizioni che consentirebbe di compilare un loop in un loop asm vuoto per una CPU normale. (Anche se il corpo non era vuoto nella sorgente, le assegnazioni alle variabili non possono essere visibili ad altri thread o gestori di segnale senza UB di data-race mentre il ciclo è in esecuzione. Quindi un'implementazione conforme potrebbe rimuovere tali corpi di ciclo se lo desiderasse a. Quindi questo lascia la questione se il loop stesso possa essere rimosso. ISO C11 dice esplicitamente di no.)
Dato che C11 individua quel caso come uno in cui l'implementazione non può presumere che il ciclo termini (e che non sia UB), sembra chiaro che intendono che il ciclo sia presente in fase di esecuzione. Un'implementazione che si rivolge alle CPU con un modello di esecuzione che non può eseguire una quantità infinita di lavoro in un tempo finito non ha giustificazione per la rimozione di un ciclo infinito costante vuoto. O anche in generale, la formulazione esatta riguarda se possono essere "ipotizzati di terminare" o meno. Se un ciclo non può terminare, ciò significa che il codice successivo non è raggiungibile, indipendentemente dagli argomenti che fai su matematica e infiniti e quanto tempo impiega a fare una quantità infinita di lavoro su una macchina ipotetica.
Inoltre, Clang non è semplicemente una DeathStation 9000 conforme a ISO C, ma è utile per la programmazione di sistemi di basso livello nel mondo reale, inclusi kernel e cose incorporate. Quindi, indipendentemente dal fatto che tu accetti o meno argomenti su C11 che ne consentono la rimozione while(1);
, non ha senso che Clang vorrebbe effettivamente farlo. Se scrivi while(1);
, probabilmente non è stato un incidente. La rimozione di loop che finiscono all'infinito per caso (con espressioni di controllo delle variabili di runtime) può essere utile e ha senso che i compilatori lo facciano.
È raro che tu voglia girare solo fino al prossimo interrupt, ma se lo scrivi in C è sicuramente quello che ti aspetti che accada. (E che cosa fa accadere in GCC e Clang, fatta eccezione per Clang quando il ciclo infinito è all'interno di una funzione wrapper).
Ad esempio, in un kernel del sistema operativo primitivo, quando lo scheduler non ha attività da eseguire, potrebbe eseguire l'attività inattiva. Una prima implementazione potrebbe essere while(1);
.
O per l'hardware senza alcuna funzione inattiva per il risparmio energetico, questa potrebbe essere l'unica implementazione. (Fino ai primi anni 2000, penso che non fosse raro su x86. Sebbene hlt
esistessero le istruzioni, IDK se risparmiasse una quantità significativa di energia fino a quando le CPU non iniziarono ad avere stati di inattività a basso consumo.)