Algunos compiladores de C hipermodernos inferirán que si un programa invocará Comportamiento indefinido cuando se le den ciertas entradas, tales entradas nunca se recibirán. En consecuencia, cualquier código que sería irrelevante a menos que se reciban tales entradas puede ser eliminado.
Como un simple ejemplo, 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);
}
un compilador puede inferir que debido a que la evaluación de value >> (32-amount)
producirá Comportamiento indefinido cuando amount
es cero, la función blah
nunca se llamará con y
igual a cero; el llamado a foo
puede ser hecho incondicional.
Por lo que puedo decir, esta filosofía parece haberse apoderado en algún momento alrededor de 2010. La evidencia más temprana que he visto de sus raíces se remonta a 2009, y se ha consagrado en el estándar C11 que establece explícitamente que si se produce un comportamiento indefinido en cualquier momento En la ejecución de un programa, el comportamiento de todo el programa retroactivamente se vuelve indefinido.
Fue la idea de que los compiladores deben intentar utilizar un comportamiento indefinido para justificar optimizaciones inversa-causales (es decir, el comportamiento no definido en la rotateleft
función debe hacer que el compilador de suponer que blah
debe haber sido llamada con un no-cero y
, o no lo volvería a hacer que y
a mantener un valor distinto de cero) ¿abogó seriamente antes de 2009? ¿Cuándo se propuso por primera vez tal cosa como una técnica de optimización?
[Apéndice]
Algunos compiladores, incluso en el siglo XX, incluyeron opciones para permitir ciertos tipos de inferencias sobre bucles y los valores calculados en ellos. Por ejemplo, dado
int i; int total=0;
for (i=n; i>=0; i--)
{
doSomething();
total += i*1000;
}
un compilador, incluso sin las inferencias opcionales, podría reescribirlo como:
int i; int total=0; int x1000;
for (i=n, x1000=n*1000; i>0; i--, x1000-=1000)
{
doSomething();
total += x1000;
}
dado que el comportamiento de ese código coincidiría con precisión con el original, incluso si el compilador especificara que los int
valores siempre se ajustan a la moda mod-65536 del complemento a dos . La opción de inferencia adicional permitiría al compilador reconocer que, dado que i
y x1000
debe cruzar cero al mismo tiempo, la primera variable se puede eliminar:
int total=0; int x1000;
for (x1000=n*1000; x1000 > 0; x1000-=1000)
{
doSomething();
total += x1000;
}
En un sistema donde los int
valores envuelven el mod 65536, un intento de ejecutar cualquiera de los dos primeros bucles con un valor n
igual a 33 resultaría en doSomething()
ser invocado 33 veces. El último bucle, por el contrario, no invocaría doSomething()
en absoluto, a pesar de que la primera invocación doSomething()
hubiera precedido cualquier desbordamiento aritmético. Tal comportamiento podría considerarse "no causal", pero los efectos están razonablemente bien restringidos y hay muchos casos en los que el comportamiento sería demostrablemente inofensivo (en los casos en que se requiere que una función produzca algún valor cuando se le da cualquier entrada, pero el valor puede ser arbitrario si la entrada no es válida, haciendo que el ciclo finalice más rápido cuando se le da un valor no válido den
en realidad sería beneficioso) Además, la documentación del compilador tendía a pedir disculpas por el hecho de que cambiaría el comportamiento de cualquier programa, incluso de aquellos que participan en UB.
Me interesa saber cuándo las actitudes de los escritores de compiladores cambiaron de la idea de que las plataformas deberían, cuando sea práctico, documentar algunas restricciones de comportamiento utilizables, incluso en casos no obligatorios por el Estándar, a la idea de que cualquier construcción que dependería de cualquier comportamiento no ordenado por el Standard debería ser calificado de ilegítimo incluso si en la mayoría de los compiladores existentes funcionaría tan bien o mejor que cualquier código estrictamente compatible que cumpla los mismos requisitos (a menudo permitiendo optimizaciones que no serían posibles en un código estrictamente compatible).
shape->Is2D()
que se invoque en un objeto que no se derivó de Shape2D
. Hay una gran diferencia entre optimizar el código que solo sería relevante si ya ha ocurrido un Comportamiento indefinido crítico versus el código que solo sería relevante en los casos en que ...
Shape2D::Is2D
es realmente mejor de lo que merece el programa.
int prod(int x, int y) {return x*y;}
habría sido suficiente. Cumplir con" no lanzar armas nucleares "de manera estrictamente compatible, requeriría un código que es más difícil de leer y casi ciertamente corre mucho más lento en muchas plataformas.