El estándar C da a los compiladores mucha libertad para realizar optimizaciones. Las consecuencias de estas optimizaciones pueden ser sorprendentes si asume un modelo ingenuo de programas donde la memoria no inicializada se establece en algún patrón de bits aleatorio y todas las operaciones se llevan a cabo en el orden en que se escriben.
Nota: los siguientes ejemplos solo son válidos porque x
nunca se toma su dirección, por lo que es "similar a un registro". También serían válidos si el tipo de x
trampa tuviera representaciones; este es raramente el caso de los tipos sin firmar (requiere "desperdiciar" al menos un bit de almacenamiento y debe estar documentado), y es imposible para unsigned char
. Si x
tuviera un tipo firmado, entonces la implementación podría definir el patrón de bits que no es un número entre - (2 n-1 -1) y 2 n-1 -1 como una representación de trampa. Vea la respuesta de Jens Gustedt .
Los compiladores intentan asignar registros a variables, porque los registros son más rápidos que la memoria. Dado que el programa puede usar más variables de las que tiene el procesador, los compiladores realizan la asignación de registros, lo que lleva a que diferentes variables utilicen el mismo registro en diferentes momentos. Considere el fragmento del programa
unsigned x, y, z;
y = 0;
z = 4;
x = - x;
y = y + z;
x = y + 1;
Cuando se evalúa la línea 3, x
aún no se inicializa, por lo tanto (razona el compilador) la línea 3 debe ser una especie de casualidad que no puede suceder debido a otras condiciones que el compilador no fue lo suficientemente inteligente para resolver. Dado z
que no se usa después de la línea 4 y x
no se usa antes de la línea 5, se puede usar el mismo registro para ambas variables. Entonces, este pequeño programa se compila para las siguientes operaciones en registros:
r1 = 0;
r0 = 4;
r0 = - r0;
r1 += r0;
r0 = r1;
El valor final de x
es el valor final de r0
y el valor final de y
es el valor final de r1
. Estos valores son x = -3 e y = -4, y no 5 y 4 como sucedería si se x
hubiera inicializado correctamente.
Para obtener un ejemplo más elaborado, considere el siguiente fragmento de código:
unsigned i, x;
for (i = 0; i < 10; i++) {
x = (condition() ? some_value() : -x);
}
Supongamos que el compilador detecta que condition
no tiene efectos secundarios. Dado condition
que no modifica x
, el compilador sabe que la primera ejecución a través del bucle no puede tener acceso x
ya que aún no está inicializado. Por lo tanto, la primera ejecución del cuerpo del bucle es equivalente a x = some_value()
, no es necesario probar la condición. El compilador puede compilar este código como si hubiera escrito
unsigned i, x;
i = 0;
x = some_value();
for (i = 1; i < 10; i++) {
x = (condition() ? some_value() : -x);
}
La forma en que esto se puede modelar dentro del compilador es considerar que cualquier valor que dependa de x
tiene el valor que sea conveniente siempre que x
no esté inicializado. Debido a que el comportamiento cuando una variable no inicializada no está definida, en lugar de que la variable simplemente tenga un valor no especificado, el compilador no necesita realizar un seguimiento de ninguna relación matemática especial entre los valores convenientes. Por lo tanto, el compilador puede analizar el código anterior de esta manera:
- durante la primera iteración del ciclo,
x
no se inicializa cuando -x
se evalúa el tiempo .
-x
tiene un comportamiento indefinido, por lo que su valor es el que sea conveniente.
- Se aplica la regla de optimización , por lo que este código se puede simplificar a .
condition ? value : value
condition; value
Cuando se enfrenta al código en su pregunta, este mismo compilador analiza que cuando x = - x
se evalúa, el valor de -x
es lo que sea conveniente. Por lo que la asignación se puede optimizar.
No he buscado un ejemplo de un compilador que se comporte como se describe arriba, pero es el tipo de optimizaciones que los buenos compiladores intentan hacer. No me sorprendería encontrarme con uno. Aquí hay un ejemplo menos plausible de un compilador con el que su programa falla. (Puede que no sea tan inverosímil si compila su programa en algún tipo de modo de depuración avanzada).
Este compilador hipotético mapea cada variable en una página de memoria diferente y configura los atributos de la página para que la lectura de una variable no inicializada provoque una trampa del procesador que invoca un depurador. Cualquier asignación a una variable primero asegura que su página de memoria esté mapeada normalmente. Este compilador no intenta realizar ninguna optimización avanzada; está en modo de depuración, destinado a localizar fácilmente errores como las variables no inicializadas. Cuando x = - x
se evalúa, el lado derecho provoca una trampa y el depurador se activa.