La regla como-si
Se permite todas y cada una de las transformaciones de código que no cambien el comportamiento observable del programa.
Contenido |
[editar]Explicación
El compilador de C++ puede realizar cualquier cambio en el programa siempre que se cumpla:
1) En cada punto de la secuencia, los valores de todos los objetos volátiles son estables (están completas las evaluaciones anteriores, no se inician las nuevas evaluaciones). | (hasta C++11) |
1) Los accesos (lecturas y escrituras) a objetos volátiles se producen estrictamente de acuerdo con la semántica de las expresiones en las que se producen. En particular, no se reordenan con respecto a otros accesos volátiles en el mismo hilo. | (desde C++11) |
ON
, los cambios en el entorno de punto flotante (excepciones de punto flotante y modos de redondeo) se garantizan para ser observados por los operadores aritméticos de punto flotante y las llamadas a función como si se ejecutaran como fueron escritos, excepto: - El resultado de cualquier expresión de punto flotante, que no sea conversión y asignación, puede tener rango y precisión de un tipo de punto flotante diferente del tipo de la expresión (véase FLT_EVAL_METHOD).
- no obstante, los resultados intermedios de cualquier expresión de punto flotante se puede calcular como si tuviera rango y precisión infinitos (a no ser que #pragma STDC FP_CONTRACT sea
OFF
).
[editar]Notas
Debido a que el compilador (normalmente) no puede analizar el código de una biblioteca externa para determinar si realiza o no E/S o acceso volátil, las llamadas a bibliotecas de terceros tampoco se ven afectadas por la optimización. Sin embargo, las llamadas a la biblioteca estándar se pueden reemplazar por otras llamadas, eliminarse o añadirse al programa durante la optimización. El código de biblioteca de terceros enlazado estáticamente puede estar sujeto a optimización en tiempo de enlazado.
Los programas con un comportamiento no especificado, por ejemplo, debido al acceso a un array fuera de límite, modificación de un objeto constante, violación del orden de evaluación, etc, están libres de la regla como-si: a menudo cambian de comportamiento observable cuando se compilan con diferentes configuraciones de optimización. Por ejemplo, si una prueba para el desbordamiento de enteros con signo se basa en el resultado de ese desbordamiento, por ejemplo: if(n+1< n) abort();, algunos compiladores lo eliminan por completo debido a que el desbordamiento de signo es un comportamiento no definido y el optimizador es libre de asumir que nunca ocurre y que la prueba es redundante.
La elisión de copia es una excepción de la regla como-si: el compilador puede eliminar las llamadas a los constructores de movimiento y de copia y las llamadas coincidentes a los destructores de objetos temporales, incluso si estas llamadas tienen efectos secundarios observables.
La expresión new tiene otra excepción de la regla como-si: el compilador puede eliminar las llamadas a funciones de asignación reemplazables, incluso si se proporciona un reemplazo definido por el usuario y tiene efectos secundarios observables. | (desde C++14) |
La cuenta y el orden de las excepciones de punto flotante se pueden cambiar mediante la optimización siempre que el estado observado pos la siguiente operación de punto flotante sea como hubiera tenido lugar ninguna optimización:
#pragma STDC FENV_ACCESS ONfor(i =0; i < n; i++) x +1;// x+1 es código muerto, pero puede generar excepciones de // punto flotante (a menos que el optimizador pueda demostrar los contrario). Sin embargo, // ejecutarlo n veces puede generar la misma excepción una y otra vez. Con lo que se puede// optimizar como:if(0< n) x +1;
[editar]Ejemplo
int& preinc(int& n){return++n;}int sumar(int n, int m){return n+m;} // entrada volátil para prevenir la optimización de constantevolatileint entrada =7; // salida volátil para que el resultado tenga efectos secundarios visiblesvolatileint resultado; int main(){int n = entrada;// el uso de operadores integrados invocaría un comportamiento no definido// int m = ++n + ++n;// pero usando funciones se garantiza que el código se ejecute como si // las funciones no estuvieran superpuestasint m = sumar(preinc(n), preinc(n)); resultado = m;}
Salida:
# código completo de la función main() creada por el compilador GCC # plataforma x86 (Intel): movl entrada(%rip), %eax # eax = entrada leal 3(%rax,%rax), %eax # eax = 3 + eax + eax movl %eax, resultado(%rip) # resultado = eax xorl %eax, %eax # eax = 0 (valor devuelto por main()) ret # plataforma PowerPC (IBM): lwz 9,LC..1(2) li 3,0 # r3 = 0 (valor devuelto por main()) lwz 11,0(9) # r11 = entrada; slwi 11,11,1 # r11 = r11 << 1; addi 0,11,3 # r0 = r11 + 3; stw 0,4(9) # resultado = r0; blr # plataforma Sparc (Sun): sethi %hi(resultado), %g2 sethi %hi(entrada), %g1 mov 0, %o0 # o0 = 0 (valor devuelto por main()) ld [%g1+%lo(entrada)], %g1 # g1 = entrada add %g1, %g1, %g1 # g1 = g1 + g1 add %g1, 3, %g1 # g1 = 3 + g1 st %g1, [%g2+%lo(resultado)] # resultado = g1 jmp %o7+8 nop # en todos los casos, los efectos secundarios de preinc() se eliminaron, y la # función main() entera se redujo al equivalente: resultado = 2*entrada + 3;