Come faccio a creare un ciclo vuoto infinito che non verrà ottimizzato?


131

Lo standard C11 sembra implicare che le istruzioni di iterazione con espressioni di controllo costanti non debbano essere ottimizzate. Sto prendendo il mio consiglio da questa risposta , che cita specificamente la sezione 6.8.5 del progetto di standard:

Un'istruzione di iterazione la cui espressione di controllo non è un'espressione costante ... può essere assunta dall'implementazione per terminare.

In quella risposta afferma che un ciclo come while(1) ;non dovrebbe essere soggetto a ottimizzazione.

Quindi ... perché Clang / LLVM ottimizza il ciclo sottostante (compilato con cc -O2 -std=c11 test.c -o test)?

#include <stdio.h>

static void die() {
    while(1)
        ;
}

int main() {
    printf("begin\n");
    die();
    printf("unreachable\n");
}

Sulla mia macchina, questo viene stampato begin, quindi si blocca su un'istruzione illegale (una ud2trappola posta dopo die()). Su Godbolt , possiamo vedere che non viene generato nulla dopo la chiamata a puts.

È stato un compito sorprendentemente difficile far sì che Clang emetta un ciclo infinito sotto -O2- mentre potrei testare ripetutamente una volatilevariabile, che comporta una lettura di memoria che non voglio. E se faccio qualcosa del genere:

#include <stdio.h>

static void die() {
    while(1)
        ;
}

int main() {
    printf("begin\n");
    volatile int x = 1;
    if(x)
        die();
    printf("unreachable\n");
}

... stampe Clang beginseguite da unreachablecome se il ciclo infinito non fosse mai esistito.

Come si ottiene a Clang l'output di un loop infinito corretto, senza accesso alla memoria con le ottimizzazioni attivate?


3
I commenti non sono per una discussione estesa; questa conversazione è stata spostata in chat .
Bhargav Rao

2
Non esiste una soluzione portatile che non comporti un effetto collaterale. Se non si desidera un accesso alla memoria, la migliore speranza sarebbe quella di registrare un carattere volatile senza segno; ma il registro scompare in C ++ 17.
Scott M,

25
Forse questo non rientra nell'ambito della domanda, ma sono curioso di sapere perché vuoi farlo. Sicuramente c'è un altro modo per realizzare il tuo vero compito. O è solo di natura accademica?
Cruncher,

1
@Cruncher: gli effetti di ogni particolare tentativo di eseguire un programma possono essere utili, essenzialmente inutili o sostanzialmente peggiori di inutili. Un'esecuzione che si traduce in un programma bloccato in un ciclo infinito può essere inutile, ma è comunque preferibile ad altri comportamenti che un compilatore potrebbe sostituire.
supercat

6
@Cruncher: perché il codice potrebbe essere in esecuzione in un contesto indipendente dove non esiste un concetto di exit(), e perché il codice potrebbe aver scoperto una situazione in cui non può garantire che gli effetti dell'esecuzione continuata non sarebbero peggiori di inutili . Un loop da saltare a sé è un modo piuttosto schifoso di gestire tali situazioni, ma può comunque essere il modo migliore di gestire una brutta situazione.
supercat

Risposte:


77

Lo standard C11 dice questo, 6.8.5 / 6:

Un'istruzione di iterazione la cui espressione di controllo non è un'espressione costante, 156) che non esegue operazioni di input / output, non accede a oggetti volatili e non esegue alcuna sincronizzazione o operazione atomica nel suo corpo, controlla l'espressione o (nel caso di un for dichiarazione) la sua espressione-3, può essere assunto dall'implementazione per terminare.157)

Le due note a piè di pagina non sono normative ma forniscono informazioni utili:

156) Un'espressione di controllo omessa è sostituita da una costante diversa da zero, che è un'espressione costante.

157) Questo ha lo scopo di consentire trasformazioni del compilatore come la rimozione di loop vuoti anche quando non è possibile provare la terminazione.

Nel tuo caso, while(1)è un'espressione costante cristallina, quindi potrebbe non esserlo l'implementazione essere considerata terminata. Una simile implementazione sarebbe irrimediabilmente interrotta, poiché i loop "per sempre" sono un costrutto di programmazione comune.

Ciò che accade al "codice irraggiungibile" dopo il ciclo, tuttavia, non è ben definito. Tuttavia, il clang si comporta davvero molto strano. Confronto del codice macchina con gcc (x86):

gcc 9.2 -O3 -std=c11 -pedantic-errors

.LC0:
        .string "begin"
main:
        sub     rsp, 8
        mov     edi, OFFSET FLAT:.LC0
        call    puts
.L2:
        jmp     .L2

clang 9.0.0 -O3 -std=c11 -pedantic-errors

main:                                   # @main
        push    rax
        mov     edi, offset .Lstr
        call    puts
.Lstr:
        .asciz  "begin"

gcc genera il loop, clang corre nel bosco ed esce con errore 255.

Sono incline a questo comportamento non conforme di clang. Perché ho cercato di espandere ulteriormente il tuo esempio in questo modo:

#include <stdio.h>
#include <setjmp.h>

static _Noreturn void die() {
    while(1)
        ;
}

int main(void) {
    jmp_buf buf;
    _Bool first = !setjmp(buf);

    printf("begin\n");
    if(first)
    {
      die();
      longjmp(buf, 1);
    }
    printf("unreachable\n");
}

Ho aggiunto C11 _Noreturn nel tentativo di aiutare ulteriormente il compilatore. Dovrebbe essere chiaro che questa funzione si bloccherà, solo da quella parola chiave.

setjmprestituirà 0 alla prima esecuzione, quindi questo programma dovrebbe semplicemente rompersi while(1)e fermarsi lì, stampando solo "start" (supponendo che \ n svuota stdout). Questo succede con gcc.

Se il ciclo è stato semplicemente rimosso, dovrebbe stampare "inizio" 2 volte, quindi stampare "non raggiungibile". Su clang comunque ( godbolt ), stampa "inizia" 1 volta e poi "non raggiungibile" prima di restituire il codice di uscita 0. Questo è semplicemente sbagliato, non importa come lo metti.

Non riesco a trovare alcun motivo per rivendicare un comportamento indefinito qui, quindi la mia opinione è che questo è un bug nel clang. Ad ogni modo, questo comportamento rende il clang inutile al 100% per programmi come i sistemi embedded, dove semplicemente devi poter fare affidamento su loop eterni che sospendono il programma (in attesa di un cane da guardia ecc.).


15
Non sono d'accordo su "questa è un'espressione costante cristallina, quindi l'implementazione potrebbe non essere considerata terminata" . Questo diventa davvero un avvocato linguistico schizzinoso, ma ha 6.8.5/6la forma di if (questi), quindi puoi supporre (questo) . Ciò non significa che in caso contrario (questi) non si può assumere (questo) . È una specifica solo per quando le condizioni sono soddisfatte, non quando non sono soddisfatte dove puoi fare quello che vuoi con gli standard. E se non ci sono osservabili ...
Kabanus il

7
@kabanus La parte citata è un caso speciale. In caso contrario (il caso speciale), valuta e ordina il codice come faresti normalmente. Se si continua a leggere lo stesso capitolo, l'espressione di controllo viene valutata come specificato per ciascuna istruzione di iterazione ("come specificato dalla semantica") ad eccezione del caso speciale citato. Segue le stesse regole della valutazione di qualsiasi calcolo di valore, che è sequenziato e ben definito.
Lundin,

2
Sono d'accordo, ma non ti aspetteresti che non int z=3; int y=2; int x=1; printf("%d %d\n", x, z);ci sia 2nell'assemblaggio, quindi nel senso inutile vuoto xnon è stato assegnato dopo yma dopo a zcausa dell'ottimizzazione. Quindi, partendo dalla tua ultima frase, seguiamo le regole regolari, ipotizziamo che il tempo si fermi (perché non eravamo vincolati meglio) e lasciato nella stampa finale "irraggiungibile". Ora ottimizziamo questa affermazione inutile (perché non conosciamo meglio).
Kabanus,

2
@MSalters Uno dei miei commenti è stato cancellato, ma grazie per l'input - e sono d'accordo. Quello che ha detto il mio commento è che penso che questo sia il cuore del dibattito - è while(1);lo stesso di int y = 2;un'affermazione in termini di quale semantica ci è permesso ottimizzare, anche se la loro logica rimane nella fonte. Da n1528 ho avuto l'impressione che possano essere uguali, ma dal momento che le persone molto più esperte di me stanno discutendo dall'altra parte, ed è apparentemente un bug ufficiale, quindi al di là di un dibattito filosofico sul fatto se la formulazione nella norma sia esplicita , l'argomento è reso controverso.
Kabanus,

2
"Una simile implementazione sarebbe irrimediabilmente interrotta, poiché i loop" per sempre "sono un costrutto di programmazione comune." - Capisco il sentimento ma l'argomento è imperfetto perché potrebbe essere applicato in modo identico al C ++, tuttavia un compilatore C ++ che ha ottimizzato questo loop non sarebbe rotto ma conforme.
Konrad Rudolph,

52

Devi inserire un'espressione che può causare un effetto collaterale.

La soluzione più semplice:

static void die() {
    while(1)
       __asm("");
}

Link Godbolt


21
Non spiega perché Clang stia agendo, tuttavia.
Lundin,

4
Basta dire "è un bug nel clang", tuttavia è sufficiente. Vorrei provare alcune cose qui prima, prima di urlare "bug".
Lundin,

3
@Lundin Non so se si tratta di un bug. Lo standard non è tecnicamente preciso in questo caso
P__J__

4
Fortunatamente, GCC è open source e posso scrivere un compilatore che ottimizza il tuo esempio. E potrei farlo per qualsiasi esempio ti venga in mente, ora e in futuro.
Thomas Weller,

3
@ThomasWeller: gli sviluppatori GCC non accetterebbero una patch che ottimizzi questo loop; violerebbe il comportamento documentato = garantito. Vedi il mio commento precedente: asm("")è implicitamente asm volatile("");e quindi l'istruzione asm deve essere eseguita tante volte quante nella macchina astratta gcc.gnu.org/onlinedocs/gcc/Basic-Asm.html . (Nota che non è sicuro che i suoi effetti collaterali includano memoria o registri; hai bisogno di Extended asm con un "memory"clobber se vuoi leggere o scrivere la memoria a cui hai mai avuto accesso da C. Basic asm è sicuro solo per cose come asm("mfence")o cli.)
Peter Cordes,

50

Altre risposte hanno già trattato i modi per far sì che Clang emetta il ciclo infinito, con linguaggio assembly incorporato o altri effetti collaterali. Voglio solo confermare che questo è davvero un bug del compilatore. In particolare, è un bug LLVM di vecchia data : applica il concetto C ++ di "tutti i loop senza effetti collaterali devono terminare" ai linguaggi dove non dovrebbe, come C.

Per esempio, il linguaggio di programmazione Rust consente anche cicli infiniti e utilizza LLVM come backend e presenta lo stesso problema.

A breve termine, sembra che LLVM continuerà ad assumere che "tutti i loop senza effetti collaterali devono terminare". Per qualsiasi linguaggio che consenta loop infiniti, LLVM si aspetta che il front-end inserisca i llvm.sideeffectcodici operativi in ​​tali loop. Questo è ciò che Rust ha intenzione di fare, quindi Clang (durante la compilazione del codice C) probabilmente dovrà farlo.


5
Niente come l'odore di un bug che è più vecchio di un decennio ... con più correzioni e patch proposte ... eppure non è stato ancora risolto.
Ian Kemp

4
@IanKemp: Per loro per correggere il bug ora sarebbe necessario riconoscere che ci sono voluti dieci anni per correggere il bug. Meglio sperare che lo Standard cambierà per giustificare il loro comportamento. Naturalmente, anche se lo standard fosse cambiato, ciò non giustificherebbe il loro comportamento se non agli occhi delle persone che considererebbero il cambiamento dello Standard come un'indicazione che il precedente mandato comportamentale dello Standard fosse un difetto che dovrebbe essere corretto retroattivamente.
supercat

4
È stato "risolto", nel senso che LLVM ha aggiunto l' sideeffectop (nel 2017) e si aspetta che i front-end inseriscano tale op in loop a loro discrezione. LLVM ha dovuto scegliere alcune impostazioni predefinite per i loop e si è scelto di scegliere quello che si allinea al comportamento di C ++, intenzionalmente o meno. Naturalmente, c'è ancora del lavoro di ottimizzazione da fare, in modo da unire le sideeffectoperazioni consecutive in una. (Questo è ciò che impedisce al front-end Rust di usarlo.) Pertanto, su quella base, il bug si trova nel front-end (clang) che non inserisce l'op nei loop.
Arnavion

@Arnavion: Esiste un modo per indicare che le operazioni possono essere rinviate a meno che o fino a quando i risultati non vengono utilizzati, ma che se i dati causano un ciclo continuo di un programma, provare a procedere oltre le dipendenze dei dati renderebbe il programma peggio che inutile ? Dover aggiungere effetti collaterali falsi che impedirebbero alle precedenti utili ottimizzazioni di impedire all'ottimizzatore di rendere un programma peggiore di quanto inutile non sembra una ricetta per l'efficienza.
supercat

Quella discussione probabilmente appartiene alle mailing list di LLVM / clang. FWIW il commit LLVM che ha aggiunto l'operazione ha anche insegnato diversi passaggi di ottimizzazione al riguardo. Inoltre, Rust ha sperimentato l'inserimento di sideeffectoperazioni all'inizio di ogni funzione e non ha riscontrato alcuna regressione delle prestazioni di runtime. L'unico problema è una regressione del tempo di compilazione , apparentemente dovuta alla mancanza di fusione di operazioni consecutive come ho menzionato nel mio commento precedente.
Arnavion,

32

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=gnu11vs. -std=gnu99non 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 asmnel 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 mainche chiama infloop() { while(1); }lo stesso puts, ma poi smette di emettere istruzioni maindopo 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 labelciclo infinito
  • oppure (se accettiamo che il ciclo infinito può essere rimosso) emette un'altra chiamata per stampare la 2a stringa e quindi return 0da 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 hltesistessero 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.)


1
Per curiosità, qualcuno sta davvero usando clang per sistemi embedded? Non l'ho mai visto e lavoro esclusivamente con embedded. gcc solo "recentemente" (10 anni fa) è entrato nel mercato embedded e io lo uso in modo scettico, preferibilmente con basse ottimizzazioni e sempre con -ffreestanding -fno-strict-aliasing. Funziona bene con ARM e forse con AVR legacy.
Lundin,

1
@Lundin: IDK su embedded, ma sì le persone costruiscono kernel con clang, almeno a volte Linux. Presumibilmente anche Darwin per MacOS.
Peter Cordes,

2
bugs.llvm.org/show_bug.cgi?id=965 questo bug sembra rilevante, ma non sono sicuro che sia quello che stiamo vedendo qui.
bracco23

1
@lundin - Sono abbastanza sicuro che abbiamo usato GCC (e molti altri toolkit) per il lavoro integrato negli anni '90, con RTOS come VxWorks e PSOS. Non capisco perché dici che GCC è entrato di recente nel mercato embedded.
Jeff Learman,

1
@JeffLearman Di recente è diventato mainstream? Ad ogni modo, il fiasco aliasing rigoroso della gcc è avvenuto solo dopo l'introduzione del C99, e le versioni più recenti di esso non sembrano più andare in banane quando incontrano violazioni rigorose dell'aliasing. Tuttavia, rimango scettico ogni volta che lo uso. Per quanto riguarda clang, l'ultima versione è evidentemente completamente rotta quando si tratta di loop eterni, quindi non può essere utilizzata per sistemi embedded.
Lundin,

14

Per la cronaca, anche Clang si comporta male con goto:

static void die() {
nasty:
    goto nasty;
}

int main() {
    int x; printf("begin\n");
    die();
    printf("unreachable\n");
}

Produce lo stesso output della domanda, ovvero:

main: # @main
  push rax
  mov edi, offset .Lstr
  call puts
.Lstr:
  .asciz "begin"

Vedo che non vedo alcun modo per leggere questo come consentito in C11, che dice solo:

6.8.6.1 (2) Un'istruzione gotoprovoca un salto incondizionato all'istruzione preceduta dall'etichetta denominata nella funzione che racchiude.

Come goto non è una "dichiarazione di iterazione" (6.8.5 liste while, doe for) nulla di speciale "terminazione-assunto" indulgenze applicare, comunque lo si voglia leggerli.

Il compilatore di link Godbolt della domanda originale è x86-64 Clang 9.0.0 e lo sono i flag -g -o output.s -mllvm --x86-asm-syntax=intel -S --gcc-toolchain=/opt/compiler-explorer/gcc-9.2.0 -fcolor-diagnostics -fno-crash-diagnostics -O2 -std=c11 example.c

Con altri come x86-64 GCC 9.2 ottieni il perfetto:

.LC0:
  .string "begin"
main:
  sub rsp, 8
  mov edi, OFFSET FLAT:.LC0
  call puts
.L2:
  jmp .L2

Flags: -g -o output.s -masm=intel -S -fdiagnostics-color=always -O2 -std=c11 example.c


Un'implementazione conforme potrebbe avere un limite di traduzione non documentato sui tempi di esecuzione o sui cicli della CPU che potrebbe causare comportamenti arbitrari se superati, o se gli input di un programma resi inevitabili superino il limite. Tali aspetti rappresentano un problema di qualità dell'attuazione, al di fuori della giurisdizione della norma. Sembrerebbe strano che i manutentori di clang siano così insistenti sul loro diritto di produrre un'implementazione di scarsa qualità, ma lo Standard lo consente.
supercat

2
@supercat grazie per il commento ... perché il superamento di un limite di traduzione non farebbe altro che fallire la fase di traduzione e rifiutarsi di eseguire? Inoltre: " 5.1.1.3 Diagnostica Un'implementazione conforme deve produrre ... messaggio diagnostico ... se un'unità di traduzione preelaborata o un'unità di traduzione contiene una violazione di qualsiasi regola o vincolo di sintassi ...". Non riesco a vedere come un comportamento errato in fase di esecuzione possa mai conformarsi.
jonathanjo,

Lo standard sarebbe completamente impossibile da implementare se i limiti di implementazione dovessero essere tutti risolti in fase di costruzione, dal momento che si potrebbe scrivere un programma Strictly Conforming che richiederebbe più byte di stack di quanti atomi ci siano nell'universo. Non è chiaro se le limitazioni di runtime debbano essere raggruppate con "limiti di traduzione", ma una tale concessione è chiaramente necessaria e non esiste un'altra categoria in cui possa essere inserita.
supercat

1
Stavo rispondendo al tuo commento sui "limiti di traduzione". Naturalmente ci sono anche limiti di esecuzione, confesso che non capisco perché stai suggerendo che dovrebbero essere ingombri di limiti di traduzione o perché dici che è necessario. Semplicemente non vedo alcun motivo per dire che nasty: goto nastypuò essere conforme e non girare la CPU (s) fino a quando interviene l'esaurimento dell'utente o delle risorse.
jonathanjo,

1
Lo standard non fa alcun riferimento ai "limiti di esecuzione" che ho potuto trovare. Cose come l'annidamento delle chiamate di funzione sono generalmente gestite dall'allocazione dello stack, ma un'implementazione conforme che limita le chiamate di funzione a una profondità di 16 potrebbe creare 16 copie di ogni funzione e far sì che una chiamata bar()all'interno foo()venga elaborata come una chiamata da __1fooa __2bar, da __2fooa __3bar, ecc e da __16fooa __launch_nasal_demons, che quindi consentire tutti gli oggetti automatiche da allocare staticamente e renderebbe ciò che è di solito un limite di "run-time" in un limite di traduzione.
supercat

5

Interpreterò l'avvocato del diavolo e sosterrò che lo standard non proibisce esplicitamente a un compilatore di ottimizzare un ciclo infinito.

Un'istruzione di iterazione la cui espressione di controllo non è un'espressione costante, 156) che non esegue operazioni di input / output, non accede a oggetti volatili e non esegue alcuna sincronizzazione o operazione atomica nel suo corpo, controlla l'espressione o (nel caso di un for dichiarazione) la sua espressione-3, può essere assunta dall'implementazione per terminare.157)

Analizziamo questo. Si può presumere che una dichiarazione di iterazione che soddisfi determinati criteri termini:

if (satisfiesCriteriaForTerminatingEh(a_loop)) 
    if (whatever_reason_or_just_because_you_feel_like_it)
         assumeTerminates(a_loop);

Questo non dice nulla su ciò che accade se i criteri non sono soddisfatti e supponendo che un ciclo possa terminare anche se non è esplicitamente vietato fintanto che vengono osservate altre regole dello standard.

do { } while(0) o while(0){} sono dopo tutte le istruzioni di iterazione (loop) che non soddisfano i criteri che consentono a un compilatore di assumere solo per un capriccio che terminano e tuttavia ovviamente terminano.

Ma il compilatore può solo ottimizzare while(1){}?

5.1.2.3p4 dice:

Nella macchina astratta, tutte le espressioni sono valutate come specificato dalla semantica. Un'implementazione effettiva non deve valutare parte di un'espressione se può dedurre che il suo valore non viene utilizzato e che non vengono prodotti effetti collaterali necessari (inclusi quelli causati dalla chiamata a una funzione o dall'accesso a un oggetto volatile).

Questo menziona espressioni, non dichiarazioni, quindi non è convincente al 100%, ma consente certamente chiamate come:

void loop(void){ loop(); }

int main()
{
    loop();
}

essere saltato. È interessante notare che Clang lo salta e CCG no .


"Questo non dice nulla su ciò che accade se i criteri non sono soddisfatti" Ma lo fa, 6.8.5.1 L'istruzione while: "La valutazione dell'espressione di controllo ha luogo prima di ogni esecuzione del corpo del loop." Questo è tutto. Questo è un calcolo del valore (di un'espressione costante), rientra nella regola della macchina astratta 5.1.2.3 che definisce il termine valutazione: "La valutazione di un'espressione in generale comprende sia il calcolo del valore sia l'inizio di effetti collaterali". E secondo lo stesso capitolo, tutte queste valutazioni sono sequenziate e valutate come specificato dalla semantica.
Lundin,

1
@Lundin Quindi while(1){}una sequenza infinita di 1valutazioni si intreccia con {}valutazioni, ma dove nello standard dice che quelle valutazioni devono richiedere un tempo diverso da zero ? Il comportamento di gcc è più utile, immagino, perché non hai bisogno di trucchi che coinvolgono l'accesso alla memoria o trucchi al di fuori della lingua. Ma non sono convinto che lo standard proibisca questa ottimizzazione in clang. Se while(1){}l'intenzione è rendere non ottimizzabile, lo standard dovrebbe essere esplicito al riguardo e il loop infinito dovrebbe essere elencato come effetto collaterale osservabile in 5.1.2.3p2.
PSkocik,

1
Penso che sia specificato, se si considera la 1condizione come un calcolo del valore. I tempi di esecuzione non contano: ciò che conta è ciò che while(A){} B;potrebbe non essere completamente ottimizzato, non ottimizzato B;e non ri-sequenziato B; while(A){}. Per citare la macchina astratta C11, enfatizza la mia: "La presenza di un punto di sequenza tra la valutazione delle espressioni A e B implica che ogni calcolo di valore ed effetto collaterale associato ad A è sequenziato prima di ogni calcolo di valore ed effetto collaterale associato a B. " Il valore di Aè chiaramente usato (dal loop).
Lundin

2
+1 Anche se mi sembra che "l'esecuzione si blocca indefinitamente senza alcun output" è un "effetto collaterale" in qualsiasi definizione di "effetto collaterale" che ha senso ed è utile al di là del semplice standard nel vuoto, questo aiuta a spiegare la mentalità da cui può avere senso per qualcuno.
mtraceur,

1
Quasi "ottimizzazione di un ciclo infinito" : non è del tutto chiaro se "esso" si riferisca allo standard o al compilatore - forse riformulare? Dato "anche se probabilmente dovrebbe" e non "sebbene probabilmente non dovrebbe" , è probabilmente lo standard a cui "si riferisce " .
Peter Mortensen,

2

Sono stato convinto che questo sia solo un vecchio bug. Lascio qui di seguito i miei test e in particolare il riferimento alla discussione in commissione standard per alcuni ragionamenti che avevo precedentemente.


Penso che questo sia un comportamento indefinito (vedi fine), e Clang ha solo un'implementazione. GCC funziona davvero come previsto, ottimizzando solo l' unreachableistruzione di stampa ma lasciando il ciclo. Alcuni come Clang prende stranamente le decisioni quando combina l'in-line e determina cosa può fare con il loop.

Il comportamento è molto strano: rimuove la stampa finale, quindi "vedendo" il ciclo infinito, ma anche liberandosi del ciclo.

È ancora peggio per quanto ne so. Rimuovendo l'inline otteniamo:

die: # @die
.LBB0_1: # =>This Inner Loop Header: Depth=1
  jmp .LBB0_1
main: # @main
  push rax
  mov edi, offset .Lstr
  call puts
.Lstr:
  .asciz "begin"

così viene creata la funzione e la chiamata ottimizzata. Questo è ancora più resistente del previsto:

#include <stdio.h>

void die(int x) {
    while(x);
}

int main() {
    printf("begin\n");
    die(1);
    printf("unreachable\n");
}

risulta in un assemblaggio non ottimale per la funzione, ma la chiamata di funzione viene nuovamente ottimizzata! Persino peggio:

void die(x) {
    while(x++);
}

int main() {
    printf("begin\n");
    die(1);
    printf("unreachable\n");
}

Ho fatto un sacco di altri test aggiungendo una variabile locale e aumentandola, passando un puntatore, usando a goto etc ... A questo punto mi sarei arreso. Se devi usare il clang

static void die() {
    int volatile x = 1;
    while(x);
}

fa il lavoro. Fa schifo all'ottimizzazione (ovviamente), e lascia nella finale ridondanteprintf . Almeno il programma non si ferma. Forse GCC dopo tutto?

appendice

In seguito alla discussione con David, ritengo che lo standard non dica "se la condizione è costante, non si può presumere che il ciclo termini". In quanto tale, e garantito dallo standard non esiste un comportamento osservabile (come definito nello standard), direi solo per coerenza: se un compilatore sta ottimizzando un ciclo perché presume che termini, non dovrebbe ottimizzare le seguenti istruzioni.

Heck n1528 ha questi come comportamento indefinito se ho letto bene. In particolare

Un grosso problema per farlo è che consente al codice di spostarsi attraverso un ciclo potenzialmente non terminante

Da qui penso che possa solo trasformarsi in una discussione su ciò che vogliamo (previsto?) Piuttosto che su ciò che è permesso.


I commenti non sono per una discussione estesa; questa conversazione è stata spostata in chat .
Bhargav Rao

Ri "plain all bug" : Intendi " plain old bug" ?
Peter Mortensen,

@PeterMortensen "ole" starebbe bene anche con me.
Kabanus

2

Sembra che questo sia un bug nel compilatore di Clang. Se non vi è alcuna costrizione sulla die()funzione di essere una funzione statica, eliminala statice falla inline:

#include <stdio.h>

inline void die(void) {
    while(1)
        ;
}

int main(void) {
    printf("begin\n");
    die();
    printf("unreachable\n");
}

Funziona come previsto quando compilato con il compilatore Clang ed è anche portatile.

Compiler Explorer (godbolt.org) - clang 9.0.0-O3 -std=c11 -pedantic-errors

main:                                   # @main
        push    rax
        mov     edi, offset .Lstr
        call    puts
.LBB0_1:                                # =>This Inner Loop Header: Depth=1
        jmp     .LBB0_1
.Lstr:
        .asciz  "begin"

Che dire static inline?
SS Anne,

1

Quanto segue sembra funzionare per me:

#include <stdio.h>

__attribute__ ((optnone))
static void die(void) {
    while (1) ;
}

int main(void) {
    printf("begin\n");
    die();
    printf("unreachable\n");
}

a godbolt

Spiegando esplicitamente a Clang di non ottimizzare quella funzione, viene emesso un ciclo infinito come previsto. Speriamo che ci sia un modo per disabilitare selettivamente particolari ottimizzazioni invece di disattivarle tutte in quel modo. Tuttavia, Clang rifiuta ancora di emettere codice per il secondo printf. Per costringerlo a farlo, ho dovuto modificare ulteriormente il codice all'interno mainper:

volatile int x = 0;
if (x == 0)
    die();

Sembra che dovrai disabilitare le ottimizzazioni per la tua funzione di ciclo infinito, quindi assicurati che il tuo ciclo infinito sia chiamato in modo condizionale. Nel mondo reale, quest'ultimo è quasi sempre il caso comunque.


1
Non è necessario che il secondo printfsia generato se il ciclo effettivamente dura per sempre, perché in quel caso il secondo printfè davvero irraggiungibile e quindi può essere eliminato. (L'errore di Clang è sia nel rilevare l'irraggiungibilità sia nell'eliminare il ciclo in modo tale da raggiungere il codice non raggiungibile).
nneonneo,

Documenti GCC __attribute__ ((optimize(1))), ma clang lo ignora come non supportato: godbolt.org/z/4ba2HM . gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html
Peter Cordes

0

Un'implementazione conforme può, e molti altri pratici, imporre limiti arbitrari sulla durata di esecuzione di un programma o su quante istruzioni eseguire e comportarsi in modo arbitrario se tali limiti vengono violati o - secondo la regola "come se" --se determina che saranno inevitabilmente violati. A condizione che un'implementazione possa elaborare con successo almeno un programma che eserciti nominalmente tutti i limiti elencati in N1570 5.2.4.1 senza colpire alcun limite di traduzione, l'esistenza di limiti, la misura in cui sono documentati e gli effetti del superamento di essi sono tutte le questioni relative alla qualità di attuazione al di fuori della giurisdizione della norma.

Penso che l'intenzione dello Standard sia abbastanza chiara che i compilatori non dovrebbero presumere che un while(1) {}ciclo senza effetti collaterali né breakdichiarazioni si fermerà. Contrariamente a quanto alcuni potrebbero pensare, gli autori dello Standard non stavano invitando gli scrittori del compilatore a essere stupidi o ottusi. Un'implementazione conforme potrebbe utilmente decidere di terminare qualsiasi programma che, se non interrotto, eseguisse più istruzioni gratuite sugli effetti collaterali di quanti sono gli atomi nell'universo, ma un'implementazione di qualità non dovrebbe eseguire tale azione sulla base di alcuna ipotesi in merito interruzione, ma piuttosto sulla base del fatto che farlo potrebbe essere utile e (diversamente dal comportamento di clang) sarebbe peggio che inutile.


-2

Il loop non ha effetti collaterali e quindi può essere ottimizzato. Il ciclo è effettivamente un numero infinito di iterazioni di zero unità di lavoro. Ciò non è definito in matematica e in logica e lo standard non dice se a un'implementazione sia permesso di completare un numero infinito di cose se ogni cosa può essere fatta in un tempo zero. L'interpretazione di Clang è perfettamente ragionevole nel considerare l'infinito per zero come zero anziché come infinito. Lo standard non dice se un ciclo infinito può terminare o meno se tutto il lavoro nei circuiti è effettivamente completato.

Al compilatore è consentito ottimizzare tutto ciò che non è un comportamento osservabile come definito nella norma. Ciò include i tempi di esecuzione. Non è necessario preservare il fatto che il ciclo, se non ottimizzato, richiederebbe una quantità infinita di tempo. È consentito cambiarlo in un tempo di esecuzione molto più breve - in effetti, il punto della maggior parte delle ottimizzazioni. Il tuo loop è stato ottimizzato.

Anche se clang ha tradotto il codice in modo ingenuo, potresti immaginare una CPU ottimizzante in grado di completare ogni iterazione in metà del tempo impiegato dall'iterazione precedente. Ciò completerebbe letteralmente il ciclo infinito in un tempo limitato. Una CPU così ottimizzata viola lo standard? Sembra abbastanza assurdo dire che una CPU ottimizzata violerebbe lo standard se fosse troppo brava a ottimizzare. Lo stesso vale per un compilatore.


I commenti non sono per una discussione estesa; questa conversazione è stata spostata in chat .
Samuel Liew

4
A giudicare dall'esperienza che hai (dal tuo profilo) posso solo concludere che questo post è scritto in malafede solo per difendere il compilatore. Stai seriamente sostenendo che qualcosa che richiede un tempo infinito può essere ottimizzato per essere eseguito in metà tempo. È ridicolo a tutti i livelli e lo sai.
pipe

@pipe: Penso che i manutentori di clang e gcc sperino che una versione futura dello standard renderà ammissibile il comportamento dei loro compilatori, e i manutentori di quei compilatori saranno in grado di fingere che un tale cambiamento fosse semplicemente una correzione di un difetto di vecchia data nella norma. Ecco come hanno trattato le garanzie della sequenza iniziale comune di C89, per esempio.
supercat

@SSAnne: Hmm ... Non credo sia sufficiente per bloccare alcune delle inferenze non chiare che gcc e clang traggono dai risultati dei confronti tra puntatore e uguaglianza.
supercat,

@supercat Ci sono <s> altri </s> tonnellate.
SS Anne,

-2

Mi dispiace se questo non è assurdamente il caso, mi sono imbattuto in questo post e lo so perché i miei anni usando la distribuzione Gentoo Linux che se vuoi che il compilatore non ottimizzi il tuo codice, dovresti usare -O0 (Zero). Ero curioso, ho compilato ed eseguito il codice sopra, e il ciclo continua a tempo indeterminato. Compilato usando clang-9:

cc -O0 -std=c11 test.c -o test

1
Il punto è creare un ciclo infinito con le ottimizzazioni abilitate.
SS Anne,

-4

Un whileloop vuoto non ha effetti collaterali sul sistema.

Pertanto Clang lo rimuove. Ci sono modi "migliori" per raggiungere il comportamento previsto che ti costringono a essere più ovvio delle tue intenzioni.

while(1); è baaadd.


6
In molti costrutti incorporati, non esiste un concetto di abort()o exit(). Se si verifica una situazione in cui una funzione determina che (forse a causa del danneggiamento della memoria) l'esecuzione continuata sarebbe peggio che pericolosa, un comportamento predefinito comune per le librerie incorporate è invocare una funzione che esegue una while(1);. Può essere utile per il compilatore avere opzioni per sostituire un comportamento più utile , ma qualsiasi scrittore di compilatore che non riesce a capire come trattare un costrutto così semplice come una barriera per l'esecuzione continua del programma è incompetente di cui fidarsi con ottimizzazioni complesse.
supercat

C'è un modo in cui puoi essere più esplicito delle tue intenzioni? l'ottimizzatore è lì per ottimizzare il tuo programma e una rimozione di loop ridondanti che non fanno nulla È un'ottimizzazione. questa è davvero una differenza filosofica tra il pensiero astratto del mondo matematico e il mondo dell'ingegneria più applicato.
Famoso Jameis il

La maggior parte dei programmi ha una serie di azioni utili che dovrebbero eseguire quando possibile e una serie di azioni peggiori che inutili che non devono mai eseguire in nessuna circostanza. Molti programmi hanno una serie di comportamenti accettabili in ogni caso particolare, uno dei quali, se il tempo di esecuzione non è osservabile, sarebbe sempre "attendere alcuni arbitrari e quindi eseguire un'azione dal set". Se tutte le azioni diverse dall'attesa
fossero

... "aspetta N + 1 secondi e poi esegui qualche altra azione", quindi il fatto che l'insieme di azioni tollerabili diverse dall'attesa sia vuoto non sarebbe osservabile. D'altra parte, se un pezzo di codice rimuove alcune azioni intollerabili dall'insieme di azioni possibili e una di quelle azioni viene comunque eseguita , ciò dovrebbe essere considerato osservabile. Sfortunatamente, le regole del linguaggio C e C ++ usano la parola "assume" in un modo strano a differenza di qualsiasi altro campo della logica o sforzo umano che posso identificare.
supercat

1
@FamousJame è ok, ma Clang non si limita a rimuovere il ciclo, ma successivamente analizza staticamente tutto come irraggiungibile ed emette un'istruzione non valida. Non è quello che ti aspetti se ha "rimosso" il loop.
nneonneo,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.