Alguns compiladores C hiper-modernos inferirão que, se um programa chamar o comportamento indefinido quando receber determinadas entradas, essas entradas nunca serão recebidas. Consequentemente, qualquer código que seja irrelevante, a menos que tais entradas sejam recebidas, poderá ser eliminado.
Como um exemplo simples, dado:
void foo(uint32_t);
uint32_t rotateleft(uint_t value, uint32_t amount)
{
return (value << amount) | (value >> (32-amount));
}
uint32_t blah(uint32_t x, uint32_t y)
{
if (y != 0) foo(y);
return rotateleft(x,y);
}
um compilador pode inferir que, como a avaliação de value >> (32-amount)
produzirá comportamento indefinido quando amount
for zero, a função blah
nunca será chamada com y
igual a zero; a chamada para foo
pode, assim, ser incondicional.
Pelo que posso dizer, essa filosofia parece ter se mantido em algum momento por volta de 2010. As primeiras evidências que eu vi de suas raízes remontam a 2009, e foram consagradas no padrão C11, que afirma explicitamente que se o Comportamento Indefinido ocorrer a qualquer momento ponto na execução de um programa, o comportamento de todo o programa retroativamente se torna indefinido.
Foi a noção de que os compiladores devem tentar usar o Comportamento indefinido para justificar otimizações causais reversas (ou seja, o Comportamento indefinido na rotateleft
função deve fazer com que o compilador assuma que blah
deve ter sido chamado com um diferente de zero y
, se alguma coisa causaria ou não y
a valor diferente de zero) seriamente defendido antes de 2009? Quando uma coisa dessas foi proposta pela primeira vez como uma técnica de otimização?
[Termo aditivo]
Alguns compiladores, mesmo no século XX, incluíram opções para permitir certos tipos de inferências sobre loops e os valores nele calculados. Por exemplo, dado
int i; int total=0;
for (i=n; i>=0; i--)
{
doSomething();
total += i*1000;
}
um compilador, mesmo sem as inferências opcionais, pode reescrevê-lo como:
int i; int total=0; int x1000;
for (i=n, x1000=n*1000; i>0; i--, x1000-=1000)
{
doSomething();
total += x1000;
}
já que o comportamento desse código corresponderia precisamente ao original, mesmo que o compilador especificasse que os int
valores sempre envolvem o mod-65536 como complemento de dois . A opção de inferência adicional permitiria ao compilador reconhecer que, uma vez que i
e x1000
deve cruzar zero ao mesmo tempo, a variável anterior pode ser eliminada:
int total=0; int x1000;
for (x1000=n*1000; x1000 > 0; x1000-=1000)
{
doSomething();
total += x1000;
}
Em um sistema em que os int
valores envolvem o mod 65536, uma tentativa de executar um dos dois primeiros loops com n
igual a 33 resultaria na doSomething()
invocação de 33 vezes. O último loop, por outro lado, não seria invocado doSomething()
, mesmo que a primeira invocação doSomething()
tivesse precedido qualquer estouro aritmético. Esse comportamento pode ser considerado "não causal", mas os efeitos são razoavelmente bem restritos e há muitos casos em que o comportamento seria comprovadamente inofensivo (nos casos em que é necessário que uma função produza algum valor quando recebe alguma entrada, mas o valor pode ser arbitrário se a entrada for inválida, fazendo com que o loop termine mais rapidamente quando receber um valor inválido den
seria realmente benéfico). Além disso, a documentação do compilador tendia a se desculpar pelo fato de alterar o comportamento de qualquer programa - mesmo daqueles envolvidos no UB.
Estou interessado em saber quando as atitudes dos escritores de compiladores se afastaram da ideia de que as plataformas deveriam documentar, na prática, algumas restrições comportamentais utilizáveis, mesmo em casos não exigidos pela Norma, para a idéia de que quaisquer construções que se baseariam em comportamentos não exigidos pela Norma. O padrão deve ter a marca ilegítima, mesmo que na maioria dos compiladores existentes funcione tão bem ou melhor do que qualquer código estritamente compatível que atenda aos mesmos requisitos (geralmente permitindo otimizações que não seriam possíveis no código estritamente compatível).
fonte
shape->Is2D()
a invocação em um objeto que não foi derivado deShape2D
. Há uma enorme diferença entre a otimização fora código que só seria relevante se um comportamento indefinido crítico já aconteceu contra o código que só seria relevante nos casos em que ...Shape2D::Is2D
é realmente melhor do que o programa merece.int prod(int x, int y) {return x*y;}
seria suficiente. Seguir" não lançar armas nucleares "de maneira estritamente compatível, no entanto, exigiria código mais difícil de ler e quase certamente correr muito mais lento em muitas plataformas.Respostas:
O comportamento indefinido é usado em situações em que não é possível que a especificação especifique o comportamento e sempre foi escrito para permitir absolutamente qualquer comportamento possível.
As regras extremamente flexíveis para o UB são úteis quando você pensa sobre o que um compilador em conformidade com as especificações deve passar. Você pode ter potência de compilação suficiente para emitir um erro ao fazer um UB ruim em um caso, mas adicione algumas camadas de recursão e agora o melhor que você pode fazer é um aviso. A especificação não tem conceito de "avisos"; portanto, se a especificação tivesse dado um comportamento, teria que ser "um erro".
A razão pela qual vemos cada vez mais efeitos colaterais disso é o impulso para a otimização. Escrever um otimizador em conformidade com especificações é difícil. Escrever um otimizador em conformidade com especificações, que também faz um trabalho notavelmente bom, adivinhar o que você pretendia quando saiu da especificação é brutal. É muito mais fácil para os compiladores se eles assumem que UB significa UB.
Isto é especialmente verdade para o gcc, que tenta oferecer suporte a muitos conjuntos de instruções com o mesmo compilador. É muito mais fácil permitir que o UB produza comportamentos UB do que tentar lidar com todas as maneiras pelas quais todos os códigos UB podem dar errado em todas as plataformas e fatorá-lo nas frases iniciais do otimizador.
fonte
x-y > z
arbitrariamente produzirá 0 ou 1 quandox-y
não for representável como "int", ela terá mais oportunidades de otimização do que uma plataforma que exige que a expressão seja escrita comoUINT_MAX/2+1+x+y > UINT_MAX/2+1+z
ou(long long)x+y > z
."O comportamento indefinido pode fazer com que o compilador reescreva o código" acontece há muito tempo, em otimizações de loop.
Faça um loop (aeb são apontadores para dobrar, por exemplo)
Nós incrementamos um int, copiamos um elemento do array, comparamos com um limite. Um compilador de otimização primeiro remove a indexação:
Removemos o caso n <= 0:
Agora eliminamos a variável i:
Agora, se n = 2 ^ 29 em um sistema de 32 bits ou 2 ^ 61 em um sistema de 64 bits, em implementações típicas, teremos o limite tmp1 == e nenhum código será executado. Agora substitua a atribuição por algo que demore muito para que o código original nunca seja executado na falha inevitável porque leva muito tempo e o compilador alterou o código.
fonte
volatile
ponteiros, portanto, o comportamento no caso em quen
é tão grande que os ponteiros seriam agrupados seria equivalente a ter um armazenamento fora dos limites desobstruir um local de armazenamento temporárioi
antes de qualquer outra coisa acontece. Sea
oub
era volátil, a plataforma documentava que os acessos voláteis geram operações físicas de carga / armazenamento na sequência solicitada e define a maneira como esses pedidos ... #i
também se tornem voláteis). Esse seria um caso de esquina comportamental bastante raro. Sea
eb
não for volátil, sugiro que não haja um significado pretendido plausível para o que o código deve fazer sen
for tão grande que substitua toda a memória. Por outro lado, muitas outras formas de UB têm significados pretendidos plausíveis.if (x-y>z) do_something()
; `não se importe se édo_something
executado em caso de estouro, desde que o estouro não tenha outro efeito. Existe alguma maneira de reescrever o de cima que não vai ...do_something
)? Mesmo que as otimizações de loop fossem proibidas de gerar comportamento inconsistente com um modelo de estouro solto, os programadores poderiam escrever código de forma a permitir que os compiladores gerassem código ideal. Existe alguma maneira de solucionar as ineficiências compelidas por um modelo "evitar transbordamento a todo custo"?Sempre foi o caso em C e C ++ que, como resultado de um comportamento indefinido, tudo pode acontecer. Portanto, também sempre foi o caso de um compilador assumir que seu código não invoca um comportamento indefinido: ou não há um comportamento indefinido em seu código, então a suposição estava correta. Ou há um comportamento indefinido no seu código; o que quer que aconteça como resultado da suposição incorreta é coberto por " tudo pode acontecer".
Se você observar o recurso "restringir" em C, o ponto principal do recurso é que o compilador pode assumir que não há comportamento indefinido; portanto, chegamos ao ponto em que o compilador não apenas pode, mas realmente deve assumir, que não há indefinido comportamento.
No exemplo que você fornece, as instruções do assembler geralmente usadas em computadores baseados em x86 para implementar o deslocamento para a esquerda ou direita mudam em 0 bits se a contagem de turnos for 32 para código de 32 bits ou 64 para código de 64 bits. Isso na maioria dos casos práticos leva a resultados indesejáveis (e resultados que não são os mesmos que no ARM ou PowerPC, por exemplo), portanto o compilador é bastante justificado para assumir que esse tipo de comportamento indefinido não ocorre. Você pode alterar seu código para
e sugira aos desenvolvedores do gcc ou Clang que, na maioria dos processadores, o código "amount == 0" seja removido pelo compilador, porque o código do assembler gerado para o código de deslocamento produzirá o mesmo resultado que o valor quando o valor == 0.
fonte
x>>y
[para não assinadox
] que funcionaria quando a variávely
mantivesse qualquer valor de 0 a 31 e fizesse algo diferente de produzir 0 oux>>(y & 31)
para outros valores, poderia ser tão eficiente quanto uma que fez outra coisa ; Não conheço nenhuma plataforma em que garantir que nenhuma outra ação que não seja uma das anteriores possa resultar em custos significativos. A idéia de que os programadores deveriam usar uma formulação mais complicada em código que nunca precisaria ser executada em máquinas obscuras seria vista como absurda.x
ou0
, ou podem ser capturadas em algumas plataformas obscuras" para "x>>32
podem fazer com que o compilador reescreva o significado de outro código"? A evidência mais antiga que posso encontrar é de 2009, mas estou curiosa para saber se existem evidências anteriores.0<=amount && amount<32
. Se valores maiores / menores fazem sentido? Eu pensei se eles fazem parte da questão. E não usar parênteses em face das operações de bit é provavelmente uma má idéia, com certeza, mas certamente não é um bug.(y mod 32)
para 32 bitsx
e(y mod 64)
64 bitsx
. Observe que é relativamente fácil emitir código que obterá um comportamento uniforme em todas as arquiteturas de CPU - mascarando a quantidade de turnos. Isso geralmente requer uma instrução extra. Mas infelizmente ...Isso ocorre porque há um erro no seu código:
Em outras palavras, ele apenas ultrapassa a barreira da causalidade se o compilador perceber que, dadas determinadas entradas, você está invocando um comportamento indefinido além da dúvida .
Ao retornar um pouco antes da invocação de comportamento indefinido, você informa ao compilador que está conscientemente impedindo a execução desse comportamento indefinido, e o compilador reconhece isso.
Em outras palavras, quando você tem um compilador que tenta aplicar a especificação de uma maneira muito estrita, precisa implementar todas as validações de argumentos possíveis no seu código. Além disso, essa validação deve ocorrer antes da invocação do referido comportamento indefinido.
Esperar! E tem mais!
Agora, com os compiladores fazendo essas coisas super-loucas, mas super-lógicas, é seu imperativo dizer ao compilador que uma função não deve continuar a execução. Assim, a
noreturn
palavra-chave nafoo()
função agora se torna obrigatória .fonte