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=gnu11
vs. -std=gnu99
nã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 asm
instruçã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 main
que chama infloop() { while(1); }
o mesmo primeiro puts
, mas depois para de emitir instruções para main
depois 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 label
loop infinito
- ou (se aceitarmos que o loop infinito pode ser removido) emite outra chamada para imprimir a segunda string e depois a
return 0
partir 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 hlt
instrução existisse, a IDK economizava uma quantidade significativa de energia até que as CPUs começassem a ter estados ociosos de baixa energia.)