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).
shape->Is2D()
a invocação em um objeto que não foi derivado de Shape2D
. 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.