Como faço para criar um loop vazio infinito que não será otimizado?

131

O padrão C11 parece sugerir que as instruções de iteração com expressões de controle constantes não devem ser otimizadas. Estou seguindo meu conselho com esta resposta , que cita especificamente a seção 6.8.5 do rascunho da norma:

Uma instrução de iteração cuja expressão de controle não é uma expressão constante ... pode ser assumida pela implementação para terminar.

Nessa resposta, menciona que um loop como while(1) ;não deve estar sujeito a otimização.

Então ... por que o Clang / LLVM otimiza o loop abaixo (compilado com 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");
}

Na minha máquina, isso é impresso begine trava em uma instrução ilegal (uma ud2armadilha colocada depois die()). No godbolt , podemos ver que nada é gerado após a chamada para puts.

Tem sido uma tarefa surpreendentemente difícil fazer com que Clang produza um loop infinito -O2- embora eu possa testar repetidamente uma volatilevariável, que envolve uma leitura de memória que eu não quero. E se eu fizer algo assim:

#include <stdio.h>

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

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

... Clang imprime beginseguido por unreachablecomo se o loop infinito nunca existisse.

Como você consegue que o Clang produza um loop infinito adequado, sem acesso à memória, com as otimizações ativadas?

nneonneo
fonte
3
Comentários não são para discussão prolongada; esta conversa foi movida para o bate-papo .
Bhargav Rao
2
Não há solução portátil que não envolva um efeito colateral. Se você não deseja acesso à memória, sua melhor esperança seria registrar caracteres não assinados voláteis; mas o registro desaparece no C ++ 17.
Scott M
25
Talvez isso não esteja no escopo da pergunta, mas estou curioso para saber por que você quer fazer isso. Certamente há outra maneira de realizar sua tarefa real. Ou isso é apenas de natureza acadêmica?
Cruncher
11
@ Cruncher: Os efeitos de qualquer tentativa específica de executar um programa podem ser úteis, essencialmente inúteis ou substancialmente piores que inúteis. Uma execução que resulta em um programa travado em um loop sem fim pode ser inútil, mas ainda assim é preferível a outros comportamentos que um compilador pode substituir.
supercat 28/01
6
@ Cruncher: porque o código pode estar sendo executado em um contexto independente, onde não há conceito exit(), e porque o código pode ter descoberto uma situação em que não pode garantir que os efeitos da execução continuada não sejam piores que inúteis . Um loop de salto para si mesmo é uma maneira bastante ruim de lidar com essas situações, mas pode ser a melhor maneira de lidar com uma situação ruim.
supercat 29/01

Respostas:

77

O padrão C11 diz isso, 6.8.5 / 6:

Uma instrução de iteração cuja expressão de controle não é uma expressão constante, 156) que não executa operações de entrada / saída, não acessa objetos voláteis e não executa operações de sincronização ou atômicas em seu corpo, expressão de controle ou (no caso de um para declaração) sua expressão-3, pode ser assumida pela implementação como finalizada. 157)

As duas notas de rodapé não são normativas, mas fornecem informações úteis:

156) Uma expressão de controle omitida é substituída por uma constante diferente de zero, que é uma expressão constante.

157) Isso visa permitir transformações do compilador, como remoção de loops vazios, mesmo quando a finalização não pode ser comprovada.

No seu caso, while(1)é uma expressão constante clara como cristal; portanto, a implementação não pode ser assumida como finalizada. Essa implementação seria irremediavelmente interrompida, pois os loops "para sempre" são uma construção de programação comum.

O que acontece com o "código inacessível" após o loop, no entanto, até onde eu sei, não está bem definido. No entanto, o clang realmente se comporta muito estranho. Comparando o código da máquina com o 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"

O gcc gera o loop, clang simplesmente entra na floresta e sai com o erro 255.

Estou inclinado a esse comportamento de clang não compatível. Porque eu tentei expandir seu exemplo ainda mais assim:

#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");
}

Adicionei C11 _Noreturnna tentativa de ajudar o compilador mais adiante. Deve ficar claro que essa função será desligada somente dessa palavra-chave.

setjmpretornará 0 na primeira execução; portanto, este programa deve simplesmente colidir com o while(1)e parar por aí, apenas imprimindo "begin" (assumindo \ n que esvazia stdout). Isso acontece com o gcc.

Se o loop foi simplesmente removido, ele deve imprimir "begin" 2 vezes e depois "inacessível". No entanto, no clang ( godbolt ), ele imprime "begin" 1 vez e depois "inacessível" antes de retornar o código de saída 0. Isso é totalmente errado, não importa como você o coloque.

Não encontro nenhum argumento para reivindicar um comportamento indefinido aqui, por isso acho que isso é um bug no clang. De qualquer forma, esse comportamento torna o clang 100% inútil para programas como sistemas embarcados, nos quais você simplesmente deve poder confiar em loops eternos pendurados no programa (enquanto aguarda um cão de guarda, etc.).

Lundin
fonte
15
Não concordo com "esta é uma expressão constante clara e cristalina, portanto, a implementação não pode ser assumida" . Isso realmente entra na advocacia de idiomas exigentes, mas 6.8.5/6é na forma de if (these), então você pode assumir (this) . Isso não significa que, se não (estes), você não pode assumir (isso) . É uma especificação apenas para quando as condições forem atendidas, não quando não forem atendidas, onde você poderá fazer o que quiser com os padrões. E se não houver observáveis ​​...
kabanus 27/01
7
@kabanus A parte citada é um caso especial. Caso contrário (o caso especial), avalie e sequencie o código como faria normalmente. Se você continuar lendo o mesmo capítulo, a expressão de controle será avaliada conforme especificado para cada instrução de iteração ("conforme especificado pela semântica"), com exceção do caso especial citado. Ele segue as mesmas regras da avaliação de qualquer cálculo de valor, que é sequenciado e bem definido.
Lundin
2
Eu concordo, mas você não ficaria surpreso que em int z=3; int y=2; int x=1; printf("%d %d\n", x, z); não há 2na montagem, portanto, no sentido inútil vazio, xnão foi atribuído depois, ymas sim, zdevido à otimização. Então, partindo da sua última frase, seguimos as regras regulares, assumimos que o tempo foi interrompido (porque não fomos constrangidos melhor) e deixados na impressão final "inacessível". Agora, otimizamos essa declaração inútil (porque não conhecemos melhor).
kabanus 27/01
2
@MSalters Um dos meus comentários foi excluído, mas obrigado pela contribuição - e eu concordo. O que meu comentário disse é que acho que esse é o cerne do debate - é while(1);o mesmo que uma int y = 2;afirmação em termos de qual semântica podemos otimizar, mesmo que sua lógica permaneça na fonte. Desde n1528, eu tive a impressão de que eles podem ser os mesmos, mas como as pessoas muito mais experientes do que eu estão discutindo o contrário, e aparentemente é um bug oficial, está além de um debate filosófico sobre se a redação do padrão é explícita , o argumento é renderizado discutível.
kabanus 28/01
2
"Essa implementação seria irremediavelmente interrompida, já que os loops 'eternos' são uma construção de programação comum". - Entendo o sentimento, mas o argumento é defeituoso porque pode ser aplicado de forma idêntica ao C ++, mas um compilador C ++ que otimizou esse loop não seria quebrado, mas sim compatível.
Konrad Rudolph
52

Você precisa inserir uma expressão que possa causar um efeito colateral.

A solução mais simples:

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

Link Godbolt

P__J__
fonte
21
No entanto, não explica por que o clang está agindo.
Lundin
4
Apenas dizer "é um bug no clang" é suficiente. Eu gostaria de tentar algumas coisas aqui antes, antes de gritar "bug".
Lundin
3
@ Lundin Eu não sei se é um bug. O padrão não é tecnicamente preciso neste caso
P__J__
4
Felizmente, o GCC é de código aberto e posso escrever um compilador que otimiza seu exemplo. E eu poderia fazer isso por qualquer exemplo que você inventar, agora e no futuro.
Thomas Weller
3
@ ThomasWeller: os desenvolvedores do GCC não aceitariam um patch que otimize esse loop; violaria o comportamento documentado = garantido. Veja meu comentário anterior: asm("")está implicitamente asm volatile("");e, portanto, a instrução asm deve ser executada tantas vezes quanto na máquina abstrata gcc.gnu.org/onlinedocs/gcc/Basic-Asm.html . (Note que é não seguro para os seus efeitos secundários para incluir qualquer memória ou registos; você precisa asm prolongado com um "memory"clobber se você quiser ler ou memória de gravação que você já acesso a partir asm C. básico só é segura para coisas como asm("mfence")ou cli.)
Peter Cordes
50

Outras respostas já abordaram maneiras de fazer Clang emitir o loop infinito, com linguagem assembly embutida ou outros efeitos colaterais. Eu só quero confirmar que este é realmente um bug do compilador. Especificamente, é um bug de longa data do LLVM - aplica o conceito C ++ de "todos os loops sem efeitos colaterais devem terminar" para idiomas em que não deveriam, como C.

Por exemplo, a linguagem de programação Rust também permite loops infinitos e usa o LLVM como back-end, e tem esse mesmo problema.

No curto prazo, parece que o LLVM continuará assumindo que "todos os loops sem efeitos colaterais devem terminar". Para qualquer idioma que permita loops infinitos, o LLVM espera que o front-end insira llvm.sideeffectcódigos de operação nesses loops. Isso é o que Rust está planejando fazer, então Clang (ao compilar o código C) provavelmente também terá que fazer isso.

Arnavion
fonte
5
Nada como o cheiro de um bug com mais de uma década ... com várias correções e patches propostos ... mas ainda não foi corrigido.
Ian Kemp
4
@IanKemp: Para que eles corrigissem o bug agora, seria necessário reconhecer que levaram dez anos para corrigi-lo. Melhor ter esperança de que o Padrão mude para justificar seu comportamento. É claro que, mesmo se o padrão mudasse, isso ainda não justificaria seu comportamento, exceto aos olhos das pessoas que considerariam a mudança no Padrão como uma indicação de que o mandato comportamental anterior do Padrão era um defeito que deveria ser corrigido retroativamente.
supercat 29/01
4
Foi "consertado" no sentido de que o LLVM adicionou a sideeffectoperação (em 2017) e espera que os front-ends insiram essa operação nos loops a seu critério. O LLVM teve que escolher algum padrão para loops e, por acaso, escolher aquele que se alinha ao comportamento do C ++, intencionalmente ou não. Obviamente, ainda há algum trabalho de otimização a ser feito, como mesclar sideeffectoperações consecutivas em uma. (É isso que está impedindo o front-end do Rust de usá-lo.) Portanto, com base nisso, o bug está no front-end (clang) que não insere o op nos loops.
Arnavion em 29/01
@ Arnavion: Existe alguma maneira de indicar que as operações podem ser adiadas, a menos ou até que os resultados sejam usados, mas que, se os dados fizerem com que um programa faça um loop infinito, tentar prosseguir com as dependências de dados anteriores tornaria o programa pior do que inútil ? Ter que adicionar efeitos colaterais falsos que impediriam as otimizações úteis anteriores para impedir que o otimizador tornasse um programa pior do que inútil não parece uma receita de eficiência.
supercat 30/01
Essa discussão provavelmente pertence às listas de discussão LLVM / clang. FWIW o commit do LLVM que adicionou o op também ensinou várias passagens de otimização sobre ele. Além disso, Rust experimentou inserir sideeffectops no início de todas as funções e não viu nenhuma regressão no desempenho em tempo de execução. O único problema é uma regressão no tempo de compilação , aparentemente devido à falta de fusão de operações consecutivas, como mencionei no meu comentário anterior.
Arnavion 30/01
32

Este é um bug do Clang

... ao embutir uma função que contém um loop infinito. O comportamento é diferente quando while(1);aparece diretamente no main, o que me cheira muito buggy.

Veja a resposta de @ Arnavion para um resumo e links. O restante desta resposta foi escrito antes que eu tivesse a confirmação de que era um bug, muito menos um bug conhecido.


Para responder à pergunta do título: Como faço para criar um loop vazio infinito que não será otimizado? ? -
crie die()uma macro, não uma função , para solucionar esse bug no Clang 3.9 e posterior. (Anteriormente, as versões Clang quer mantém o loop ou emite umcall para uma versão não-inline da função com o loop infinito.) Isso parece ser segura, mesmo que as print;while(1);print;funções inlines em seu chamador ( Godbolt ). -std=gnu11vs. -std=gnu99não muda nada.

Se você se importa apenas com o GNU C, os P__J____asm__(""); dentro do loop também funcionam e não prejudicam a otimização de nenhum código circundante para nenhum compilador que o entenda. As instruções GNU C Basic asm são implicitamentevolatile , portanto, isso conta como um efeito colateral visível que deve ser "executado" quantas vezes for na máquina abstrata C. (E sim, Clang implementa o dialeto GNU de C, conforme documentado no manual do GCC.)


Algumas pessoas argumentaram que pode ser legal otimizar um loop infinito vazio. Não concordo 1 , mas mesmo que aceitemos isso, também não pode ser legal para Clang assumir declarações após o loop estar inacessível, e deixar a execução cair do final da função para a próxima função ou para o lixo que decodifica como instruções aleatórias.

(Isso seria compatível com os padrões do Clang ++ (mas ainda não é muito útil); loops infinitos sem efeitos colaterais são UB no C ++, mas não no C.
É enquanto (1); comportamento indefinido no C? UB permite que o compilador emita basicamente qualquer coisa para código em um caminho de execução que definitivamente encontrará UB. Uma asminstrução no loop evitaria esse UB para C ++. Mas, na prática, a compilação de Clang como C ++ não remove loops vazios infinitos de expressão constante, exceto quando embutidos, o mesmo que quando compilando como C.)


A inclusão manual de while(1);alterações altera a forma como o Clang o compila: loop infinito presente no asm. É o que esperávamos de um advogado de regras POV.

#include <stdio.h>
int main() {
    printf("begin\n");
    while(1);
    //infloop_nonconst(1);
    //infloop();
    printf("unreachable\n");
}

No explorador do compilador Godbolt , Clang 9.0 -O3 compilando como C ( -xc) para 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"

O mesmo compilador com as mesmas opções compila um mainque chama infloop() { while(1); }o mesmo primeiro puts, mas depois para de emitir instruções para maindepois desse ponto. Então, como eu disse, a execução cai do final da função para qualquer função seguinte (mas com a pilha desalinhada para a entrada da função, não é nem um tailcall válido).

As opções válidas seriam

  • emitir um label: jmp labelloop infinito
  • ou (se aceitarmos que o loop infinito pode ser removido) emite outra chamada para imprimir a segunda string e depois a return 0partir de main.

Deixar de funcionar ou continuar sem imprimir "inacessível" claramente não é aceitável para uma implementação C11, a menos que haja UB que eu não tenha notado.


Nota de rodapé 1:

Para constar, concordo com a resposta de @ Lundin, que cita o padrão de evidência de que C11 não permite a suposição de terminação para loops infinitos de expressão constante, mesmo quando eles estão vazios (sem E / S, voláteis, sincronização ou outros efeitos secundários visíveis).

Este é o conjunto de condições que permitiriam que um loop fosse compilado em um loop asm vazio para uma CPU normal. (Mesmo que o corpo não estivesse vazio na fonte, as atribuições para variáveis ​​não podem ser visíveis para outros threads ou manipuladores de sinal sem o UB de corrida de dados enquanto o loop estiver em execução. Portanto, uma implementação em conformidade pode remover esses corpos de loop, se desejado Isso deixa a questão de saber se o próprio loop pode ser removido. A ISO C11 diz explicitamente que não.)

Dado que o C11 destaca esse caso como um caso em que a implementação não pode assumir que o loop termina (e que não é UB), parece claro que eles pretendem que o loop esteja presente no tempo de execução. Uma implementação que tem como alvo CPUs com um modelo de execução que não pode executar uma quantidade infinita de trabalho em tempo finito não tem justificativa para remover um loop infinito constante vazio. Ou mesmo em geral, o texto exato é sobre se eles podem ser "supostamente terminados" ou não. Se um loop não pode terminar, isso significa que o código posterior não está acessível, independentemente dos argumentos que você faz sobre matemática e infinitos e quanto tempo leva para realizar uma quantidade infinita de trabalho em alguma máquina hipotética.

Além disso, o Clang não é apenas um DeathStation 9000 compatível com ISO C, ele deve ser útil para a programação de sistemas de baixo nível no mundo real, incluindo kernels e outras coisas incorporadas. Portanto, independentemente de você aceitar ou não argumentos sobre o C11 permitindo a remoção while(1);, não faz sentido que Clang realmente queira fazer isso. Se você escreve while(1);, isso provavelmente não foi um acidente. A remoção de loops que acabam infinitos por acidente (com expressões de controle de variáveis ​​em tempo de execução) pode ser útil e faz sentido que os compiladores façam isso.

É raro você querer girar até a próxima interrupção, mas se você escrever isso em C, é definitivamente o que você espera que aconteça. (E o que acontece no GCC e no Clang, exceto no Clang quando o loop infinito está dentro de uma função do wrapper).

Por exemplo, em um kernel do sistema operacional primitivo, quando o planejador não possui tarefas para executar, ele pode executar a tarefa ociosa. Uma primeira implementação disso pode ser while(1);.

Ou para hardware sem nenhum recurso ocioso de economia de energia, essa pode ser a única implementação. (Até o início dos anos 2000, acho que isso não é raro no x86. Embora a hltinstrução existisse, a IDK economizava uma quantidade significativa de energia até que as CPUs começassem a ter estados ociosos de baixa energia.)

Peter Cordes
fonte
11
Por curiosidade, alguém está realmente usando clang para sistemas embarcados? Eu nunca vi isso e trabalho exclusivamente com incorporado. O gcc apenas "recentemente" (10 anos atrás) entrou no mercado incorporado e eu o uso ceticamente, de preferência com baixas otimizações e sempre com -ffreestanding -fno-strict-aliasing. Funciona bem com o ARM e talvez com o AVR herdado.
Lundin
11
@Lundin: IDK sobre incorporado, mas sim, as pessoas constroem kernels com clang, pelo menos às vezes Linux. Presumivelmente, também Darwin para MacOS.
Peter Cordes
2
bugs.llvm.org/show_bug.cgi?id=965 esse bug parece relevante, mas não tenho certeza se é o que estamos vendo aqui.
bracco23 28/01
11
@lundin - Tenho certeza de que usamos o GCC (e muitos outros kits de ferramentas) para trabalhos incorporados nos anos 90, com RTOS como VxWorks e PSOS. Não entendo por que você diz que o GCC entrou recentemente no mercado incorporado recentemente.
Jeff Learman 19/02
11
@JeffLearman Tornou-se mainstream recentemente, então? De qualquer forma, o fiasco estrito de aliasing do gcc só aconteceu após a introdução do C99, e versões mais recentes dele não parecem mais bananas ao encontrar violações estrias de aliasing. Ainda assim, continuo cético sempre que o uso. Quanto ao clang, a versão mais recente está evidentemente completamente quebrada quando se trata de loops eternos, portanto não pode ser usada para sistemas embarcados.
Lundin
14

Apenas para constar, Clang também se comporta mal com goto:

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

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

Produz a mesma saída da pergunta, ou seja:

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

Não vejo nenhuma maneira de ler isso como permitido em C11, que apenas diz:

6.8.6.1 (2) A goto instrução causa um salto incondicional na instrução prefixada pelo rótulo nomeado na função anexa.

Como gotonão é uma "declaração de iteração" (6.8.5 lista while,do e for) nada sobre as indulgências especiais "assumidas por terminação" se aplicam, no entanto, você deseja lê-las.

O compilador de link Godbolt da pergunta original é x86-64 Clang 9.0.0 e os sinalizadores são -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

Em outros, como o x86-64 GCC 9.2, você fica perfeito:

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

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

jonathanjo
fonte
Uma implementação em conformidade pode ter um limite de conversão não documentado no tempo de execução ou em ciclos da CPU que podem causar comportamento arbitrário se excedido, ou se as entradas de um programa tornarem inevitável o excedente do limite. Essas coisas são um problema de qualidade de implementação, fora da jurisdição da norma. Parece estranho que os mantenedores do clang sejam tão insistentes em seu direito de produzir uma implementação de baixa qualidade, mas o Padrão permite isso.
supercat 29/01
2
@ supercat obrigado pelo comentário ... por que exceder um limite de tradução faria outra coisa senão falhar na fase de tradução e se recusar a executar? Além disso: " 5.1.1.3 Diagnósticos Uma implementação em conformidade deve produzir ... mensagem de diagnóstico ... se uma unidade de tradução ou unidade de tradução com pré-processamento contiver uma violação de qualquer regra ou restrição de sintaxe ...". Não vejo como um comportamento errado na fase de execução pode estar em conformidade.
jonathanjo
A Norma seria completamente impossível de implementar se todos os limites de implementação tivessem que ser resolvidos no momento da construção, uma vez que se poderia escrever um programa Estritamente Conformista que exigiria mais bytes de pilha do que átomos no universo. Não está claro se as limitações de tempo de execução devem ser agrupadas com "limites de conversão", mas essa concessão é claramente necessária e não há outra categoria na qual ela possa ser colocada.
supercat 29/01
11
Eu estava respondendo ao seu comentário sobre "limites de tradução". É claro que também existem limites de execução, confesso que não entendo por que você está sugerindo que eles devam ser agrupados com limites de tradução ou por que você diz que é necessário. Eu simplesmente não vejo nenhuma razão para dizer que nasty: goto nastypode estar em conformidade e não girar a (s) CPU (s) até que a exaustão do usuário ou do recurso interfira.
jonathanjo
11
O Padrão não faz referência aos "limites de execução" que eu pude encontrar. Coisas como função de chamada de nidificação são geralmente tratadas pelos alocação de pilha, mas uma aplicação conformes que os limites de chamadas de função para uma profundidade de 16 poderia construir 16 cópias de cada função, e ter uma chamada para bar()dentro de foo()ser processado como uma chamada a partir __1foode __2bar, a partir __2foode __3bar, etc. e from __16footo __launch_nasal_demons, o que permitiria que todos os objetos automáticos fossem alocados estaticamente e transformaria o que geralmente é um limite de "tempo de execução" em um limite de conversão.
supercat 29/01
5

Vou interpretar o advogado do diabo e argumentar que o padrão não proíbe explicitamente um compilador de otimizar um loop infinito.

Uma instrução de iteração cuja expressão de controle não é uma expressão constante, 156) que não executa operações de entrada / saída, não acessa objetos voláteis e não executa operações de sincronização ou atômicas em seu corpo, expressão de controle ou (no caso de um para ), sua expressão-3 pode ser assumida pela implementação para terminar.157)

Vamos analisar isso. Uma declaração de iteração que atenda a certos critérios pode ser assumida como finalizada:

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

Isso não diz nada sobre o que acontece se os critérios não forem satisfeitos e assumir que um loop pode terminar mesmo assim não é explicitamente proibido, desde que outras regras do padrão sejam observadas.

do { } while(0) ou while(0){} são todas as instruções de iteração (loops) que não atendem aos critérios que permitem que um compilador assuma apenas por um capricho que ele termina e, no entanto, obviamente termina.

Mas o compilador pode otimizar while(1){} ?

5.1.2.3p4 diz:

Na máquina abstrata, todas as expressões são avaliadas conforme especificado pela semântica. Uma implementação real não precisa avaliar parte de uma expressão se puder deduzir que seu valor não é usado e que nenhum efeito colateral necessário é produzido (incluindo os causados ​​pela chamada de uma função ou pelo acesso a um objeto volátil).

Isso menciona expressões, não declarações, por isso não é 100% convincente, mas certamente permite chamadas como:

void loop(void){ loop(); }

int main()
{
    loop();
}

para ser pulado. Curiosamente, o clang ignora e o gcc não .

PSkocik
fonte
"Isso não diz nada sobre o que acontece se os critérios não forem atendidos". 6.8.5.1. A declaração while: "A avaliação da expressão de controle ocorre antes de cada execução do corpo do loop". É isso aí. Este é um cálculo de valores (de uma expressão constante), está sob a regra da máquina abstrata 5.1.2.3 que define o termo avaliação: "A avaliação de uma expressão em geral inclui cálculos de valores e início de efeitos colaterais". E de acordo com o mesmo capítulo, todas essas avaliações são sequenciadas e avaliadas conforme especificado pela semântica.
Lundin
11
@Lundin Então, while(1){}há uma sequência infinita de 1avaliações entrelaçada com {}avaliações, mas onde, no padrão, diz que essas avaliações precisam levar um tempo diferente de zero ? O comportamento do gcc é mais útil, eu acho, porque você não precisa de truques que envolvam acesso à memória ou truques fora do idioma. Mas não estou convencido de que o padrão proíba essa otimização em clang. Se tornar while(1){}a intenção não otimizável é a intenção, o padrão deve ser explícito e o loop infinito deve ser listado como um efeito colateral observável em 5.1.2.3p2.
PSkocik 27/01
11
Eu acho que é especificado, se você tratar a 1condição como um cálculo de valor. O tempo de execução não importa - o que importa é o que while(A){} B;pode não ser totalmente otimizado, não otimizado B;e não sequenciado novamente B; while(A){}. Para citar a máquina abstrata C11, enfatize a minha: "A presença de um ponto de sequência entre a avaliação das expressões A e B implica que todo cálculo de valor e efeito colateral associado a A sejam seqüenciados antes de todo cálculo de valor e efeito colateral associado a B ". O valor de Aé claramente usado (pelo loop).
Lundin
2
+1 Embora pareça para mim "a execução travar indefinidamente sem saída" é um "efeito colateral" em qualquer definição de "efeito colateral" que faça sentido e seja útil além do padrão no vácuo, isso ajuda a explicar a mentalidade a partir da qual isso pode fazer sentido para alguém.
mtraceur 27/01
11
Perto de "otimizar um loop infinito" : não está totalmente claro se "it" se refere ao padrão ou ao compilador - talvez reformular? Dado "embora provavelmente deva" e não "embora provavelmente não deva" , é provavelmente o padrão a que "ele" se refere.
Peter Mortensen
2

Estou convencido de que este é apenas um bug antigo. Deixo os meus testes abaixo e, em particular, a referência à discussão no comitê padrão por algum raciocínio que eu já tinha.


Eu acho que esse é um comportamento indefinido (veja final), e Clang tem apenas uma implementação. O GCC realmente funciona como você espera, otimizando apenas ounreachable declaração de impressão, mas deixando o loop. De alguma forma, como Clang está estranhamente tomando decisões ao combinar o alinhamento e determinar o que ele pode fazer com o loop.

O comportamento é muito estranho - ele remove a impressão final, então "vendo" o loop infinito, mas depois se livrando do loop também.

É ainda pior, até onde eu sei. Removendo o inline, obtemos:

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"

para que a função seja criada e a chamada otimizada. Isso é ainda mais resistente do que o esperado:

#include <stdio.h>

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

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

resulta em uma montagem não ideal para a função, mas a chamada de função é novamente otimizada! Pior ainda:

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

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

Fiz vários outros testes adicionando uma variável local e aumentando-a, passando um ponteiro, usando um gotoetc ... Nesse ponto, desistiria. Se você deve usar clang

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

faz o trabalho. É péssimo em otimizar (obviamente) e sai na final redundante printf. Pelo menos o programa não para. Talvez o GCC, afinal?

Termo aditivo

Após discussão com David, concluo que o padrão não diz "se a condição for constante, você não pode assumir que o loop termina". Como tal, e concedido de acordo com o padrão, não há comportamento observável (conforme definido no padrão), eu argumentaria apenas pela consistência - se um compilador estiver otimizando um loop porque assume que termina, ele não deve otimizar as instruções a seguir.

Heck n1528 tem isso como comportamento indefinido, se eu leio direito. Especificamente

Uma questão importante para fazer isso é que ele permite que o código se mova através de um loop potencialmente não terminável

A partir daqui, acho que só pode evoluir para uma discussão sobre o que queremos (esperado?) E não o que é permitido.

kabanus
fonte
Comentários não são para discussão prolongada; esta conversa foi movida para o bate-papo .
Bhargav Rao
Re "plain all bug" : Você quer dizer " plain old bug" ?
Peter Mortensen
@ PeterMortensen "ole" também ficaria bem comigo.
kabanus 29/01
2

Parece que isso é um bug no compilador Clang. Se não houver nenhuma compulsão na die()função de ser uma função estática, elimine statice faça isso inline:

#include <stdio.h>

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

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

Está funcionando como esperado quando compilado com o compilador Clang e também é portátil.

Explorador de compilador (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"
HS
fonte
Que tal static inline?
SS Anne
1

O seguinte parece funcionar para mim:

#include <stdio.h>

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

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

às godbolt

Dizendo explicitamente a Clang para não otimizar que uma função faça com que um loop infinito seja emitido conforme o esperado. Espero que haja uma maneira de desativar seletivamente otimizações específicas em vez de desativá-las dessa maneira. Clang ainda se recusa a emitir código para o segundo printf, no entanto. Para forçá-lo a fazer isso, tive que modificar ainda mais o código interno mainpara:

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

Parece que você precisará desativar as otimizações para sua função de loop infinito e garantir que seu loop infinito seja chamado condicionalmente. No mundo real, este último é quase sempre o caso.

bta
fonte
11
Não é necessário que o segundo printfseja gerado se o loop realmente durar para sempre, porque nesse caso o segundo printfé realmente inacessível e, portanto, pode ser excluído. (O erro de Clang consiste em detectar a inacessibilidade e, em seguida, excluir o loop para que o código inacessível seja alcançado).
nneonneo 28/01
Documentos do GCC __attribute__ ((optimize(1))), mas o clang o ignora como não suportado: godbolt.org/z/4ba2HM . gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html
Peter Cordes
0

Uma implementação em conformidade pode, e muitas práticas, impõem limites arbitrários sobre quanto tempo um programa pode executar ou quantas instruções ele executa e se comporta de maneira arbitrária se esses limites forem violados ou - sob a regra "como se" --se determinar que elas serão inevitavelmente violadas. Desde que uma implementação possa processar com êxito pelo menos um programa que exercite nominalmente todos os limites listados na N1570 5.2.4.1 sem atingir nenhum limite de conversão, a existência de limites, a extensão em que eles estão documentados e os efeitos de excedê-los. todos os problemas de Qualidade de Implementação fora da jurisdição da Norma.

Penso que a intenção da Norma é bastante clara de que os compiladores não devem assumir que um while(1) {}loop sem efeitos colaterais nem breakdeclarações terminará. Ao contrário do que algumas pessoas pensam, os autores do Padrão não estavam convidando os escritores de compiladores a serem estúpidos ou obtusos. Uma implementação em conformidade pode ser útil decidir encerrar qualquer programa que, se não for interrompido, executará mais instruções livres de efeitos colaterais do que átomos no universo, mas uma implementação de qualidade não deve executar tal ação com base em qualquer suposição sobre terminação, mas com base no fato de que isso poderia ser útil e não seria (ao contrário do comportamento de Clang) pior do que inútil.

supercat
fonte
-2

O loop não tem efeitos colaterais e, portanto, pode ser otimizado. O loop é efetivamente um número infinito de iterações de zero unidades de trabalho. Isso é indefinido em matemática e em lógica, e o padrão não diz se é permitido a uma implementação concluir um número infinito de coisas se cada coisa puder ser feita em tempo zero. A interpretação de Clang é perfeitamente razoável no tratamento do infinito vezes zero como zero, em vez de infinito. O padrão não diz se um loop infinito pode ou não terminar se todo o trabalho nos loops for realmente concluído.

É permitido ao compilador otimizar qualquer coisa que não seja um comportamento observável, conforme definido no padrão. Isso inclui tempo de execução. Não é necessário preservar o fato de que o loop, se não for otimizado, levaria uma quantidade infinita de tempo. É permitido mudar isso para um tempo de execução muito menor - na verdade, esse é o ponto da maioria das otimizações. Seu loop foi otimizado.

Mesmo que o clang tenha traduzido o código ingenuamente, você pode imaginar uma CPU otimizada que pode concluir cada iteração na metade do tempo que a iteração anterior levou. Isso literalmente completaria o loop infinito em uma quantidade finita de tempo. Essa CPU otimizadora viola o padrão? Parece bastante absurdo dizer que uma CPU otimizada violaria o padrão se for muito boa para otimizar. O mesmo vale para um compilador.

David Schwartz
fonte
Comentários não são para discussão prolongada; esta conversa foi movida para o bate-papo .
Samuel Liew
4
A julgar pela experiência que você tem (pelo seu perfil), só posso concluir que este post foi escrito de má-fé apenas para defender o compilador. Você está argumentando seriamente que algo que leva uma quantidade infinita de tempo pode ser otimizado para ser executado na metade do tempo. Isso é ridículo em todos os níveis e você sabe disso.
pipe
@pipe: Eu acho que os mantenedores do clang e do gcc esperam que uma versão futura do Padrão torne o comportamento de seus compiladores permitido, e os mantenedores desses compiladores poderão fingir que essa mudança foi apenas uma correção de um defeito de longa data. no padrão. Foi assim que eles trataram as garantias Common Initial Sequence do C89, por exemplo.
supercat 29/01
@SSAnne: Hmm ... acho que isso não é suficiente para bloquear algumas das inferências doentias que o gcc e o clang extraem dos resultados das comparações entre ponteiros e igualdade.
supercat 21/02
@supercat Existem <s> outros </s> toneladas.
SS Anne
-2

Me desculpe se esse absurdo não for o caso, eu me deparei com este post e sei que, durante os meus anos usando a distribuição do Gentoo Linux, se você quiser que o compilador não otimize seu código, use -O0 (Zero). Fiquei curioso e compilei e executei o código acima, e o loop continua indefinidamente. Compilado usando clang-9:

cc -O0 -std=c11 test.c -o test
Fellipe Weno
fonte
11
O objetivo é fazer um loop infinito com as otimizações ativadas.
SS Anne
-4

Um whileloop vazio não tem efeitos colaterais no sistema.

Portanto, o Clang o remove. Existem maneiras "melhores" de alcançar o comportamento pretendido que o forçam a ser mais óbvio de suas intenções.

while(1); é baaadd.

Famous Jameis
fonte
6
Em muitas construções incorporadas, não há conceito de abort()ou exit(). Se surgir uma situação em que uma função determine que (talvez como resultado de corrupção de memória) a execução continuada seja pior que perigosa, um comportamento padrão comum para bibliotecas incorporadas é invocar uma função que execute a while(1);. Pode ser útil para o compilador ter opções para substituir um comportamento mais útil , mas qualquer gravador de compilador que não consiga descobrir como tratar uma construção tão simples como uma barreira à execução contínua do programa é incompetente em ser otimizado por otimizações complexas.
supercat 29/01
Existe uma maneira de você ser mais explícito de suas intenções? o otimizador existe para otimizar seu programa e remover loops redundantes que não fazem nada É uma otimização. essa é realmente uma diferença filosófica entre o pensamento abstrato do mundo da matemática e o mundo da engenharia mais aplicada.
Famous Jameis
A maioria dos programas possui um conjunto de ações úteis que devem ser executadas quando possível e um conjunto de ações piores que inúteis que nunca devem ser executadas sob nenhuma circunstância. Muitos programas têm um conjunto de comportamentos aceitáveis ​​em qualquer caso específico, um dos quais, se o tempo de execução não for observável, sempre seria "esperar alguns arbitrários e depois executar algumas ações do conjunto". Se todas as ações, exceto a espera, estiverem no conjunto de ações piores que inúteis, não haveria um número de segundos N pelos quais "esperar para sempre" seria notavelmente diferente de ...
supercat 30/01
... "aguarde N + 1 segundos e execute outra ação", para que o conjunto de ações toleráveis ​​que não seja a espera esteja vazio não seja observável. Por outro lado, se um pedaço de código remover alguma ação intolerável do conjunto de ações possíveis, e uma dessas ações for executada de qualquer maneira , isso deve ser considerado observável. Infelizmente, as regras da linguagem C e C ++ usam a palavra "assumir" de uma maneira estranha, diferente de qualquer outro campo da lógica ou esforço humano que eu possa identificar.
supercat 30/01
11
@FamousJameis ok, mas Clang não remove apenas o loop - ele analisa estaticamente tudo depois como inacessível e emite uma instrução inválida. Não é o que você espera se apenas "removeu" o loop.
nneonneo 14/02