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 begin
e trava em uma instrução ilegal (uma ud2
armadilha 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 volatile
variá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 begin
seguido por unreachable
como 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?
fonte
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.Respostas:
O padrão C11 diz isso, 6.8.5 / 6:
As duas notas de rodapé não são normativas, mas fornecem informações úteis:
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
clang 9.0.0
-O3 -std=c11 -pedantic-errors
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:
Adicionei C11
_Noreturn
na tentativa de ajudar o compilador mais adiante. Deve ficar claro que essa função será desligada somente dessa palavra-chave.setjmp
retornará 0 na primeira execução; portanto, este programa deve simplesmente colidir com owhile(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.).
fonte
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 ...int z=3; int y=2; int x=1; printf("%d %d\n", x, z);
não há2
na montagem, portanto, no sentido inútil vazio,x
não foi atribuído depois,y
mas sim,z
devido à 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).while(1);
o mesmo que umaint 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.Você precisa inserir uma expressão que possa causar um efeito colateral.
A solução mais simples:
Link Godbolt
fonte
asm("")
está implicitamenteasm 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 comoasm("mfence")
oucli
.)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.sideeffect
có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.fonte
sideeffect
operaçã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 mesclarsideeffect
operaçõ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.sideeffect
ops 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.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 asprint;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.No explorador do compilador Godbolt , Clang 9.0 -O3 compilando como C (
-xc
) para x86-64:O mesmo compilador com as mesmas opções compila um
main
que chamainfloop() { while(1); }
o mesmo primeiroputs
, mas depois para de emitir instruções paramain
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
label: jmp label
loop infinitoreturn 0
partir demain
.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ê escrevewhile(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.)fonte
-ffreestanding -fno-strict-aliasing
. Funciona bem com o ARM e talvez com o AVR herdado.Apenas para constar, Clang também se comporta mal com
goto
:Produz a mesma saída da pergunta, ou seja:
Não vejo nenhuma maneira de ler isso como permitido em C11, que apenas diz:
Como
goto
não é uma "declaração de iteração" (6.8.5 listawhile
,do
efor
) 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:
Bandeiras:
-g -o output.s -masm=intel -S -fdiagnostics-color=always -O2 -std=c11 example.c
fonte
nasty: goto nasty
pode estar em conformidade e não girar a (s) CPU (s) até que a exaustão do usuário ou do recurso interfira.bar()
dentro defoo()
ser processado como uma chamada a partir__1foo
de__2bar
, a partir__2foo
de__3bar
, etc. e from__16foo
to__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.Vou interpretar o advogado do diabo e argumentar que o padrão não proíbe explicitamente um compilador de otimizar um loop infinito.
Vamos analisar isso. Uma declaração de iteração que atenda a certos critérios pode ser assumida como finalizada:
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)
ouwhile(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:
Isso menciona expressões, não declarações, por isso não é 100% convincente, mas certamente permite chamadas como:
para ser pulado. Curiosamente, o clang ignora e o gcc não .
fonte
while(1){}
há uma sequência infinita de1
avaliaçõ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 tornarwhile(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.1
condição como um cálculo de valor. O tempo de execução não importa - o que importa é o quewhile(A){} B;
pode não ser totalmente otimizado, não otimizadoB;
e não sequenciado novamenteB; 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 deA
é claramente usado (pelo loop).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 o
unreachable
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:
para que a função seja criada e a chamada otimizada. Isso é ainda mais resistente do que o esperado:
resulta em uma montagem não ideal para a função, mas a chamada de função é novamente otimizada! Pior ainda:
Fiz vários outros testes adicionando uma variável local e aumentando-a, passando um ponteiro, usando um
goto
etc ... Nesse ponto, desistiria. Se você deve usar clangfaz 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
A partir daqui, acho que só pode evoluir para uma discussão sobre o que queremos (esperado?) E não o que é permitido.
fonte
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, eliminestatic
e faça issoinline
: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
fonte
static inline
?O seguinte parece funcionar para mim:
à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 internomain
para: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.
fonte
printf
seja gerado se o loop realmente durar para sempre, porque nesse caso o segundoprintf
é 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).__attribute__ ((optimize(1)))
, mas o clang o ignora como não suportado: godbolt.org/z/4ba2HM . gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.htmlUma 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 nembreak
declaraçõ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.fonte
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.
fonte
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:
fonte
Um
while
loop 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.fonte
abort()
ouexit()
. 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 awhile(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.