software optimizar optimizacion niveles muerto ejemplos codigo c++ c gcc compiler-optimization

c++ - optimizar - ¿Pueden los diferentes niveles de optimización generar códigos funcionalmente diferentes?



optimizacion de software (10)

Tengo curiosidad sobre las libertades que tiene un compilador al optimizar. Limitemos esta pregunta a GCC y C / C ++ (cualquier versión, cualquier sabor de estándar):

¿Es posible escribir código que se comporte de manera diferente según el nivel de optimización con el que se compiló?

El ejemplo que tengo en mente es imprimir diferentes bits de texto en varios constructores en C ++ y obtener una diferencia dependiendo de si se eliminan las copias (aunque no he podido hacer que algo funcione).

No está permitido contar los ciclos del reloj. Si tiene un ejemplo para un compilador no GCC, también sería curioso, pero no puedo verificarlo. Puntos de bonificación para un ejemplo en C. :-)

Editar: El código de ejemplo debe ser compatible con estándares y no debe contener un comportamiento indefinido desde el principio.

Edición 2: ¡Ya tienes algunas respuestas excelentes! Déjame subir un poco: el código debe constituir un programa bien formado y cumplir con los estándares, y debe compilar para corregir los programas deterministas en cada nivel de optimización. (Eso excluye cosas como condiciones de carrera en código multiproceso mal formado.) También aprecio que el redondeo de punto flotante puede verse afectado, pero descartemos eso.

Acabo de alcanzar la reputación de 800, así que creo que voy a arruinar mi reputación como recompensa en el primer ejemplo completo para conformarme con (el espíritu) de esas condiciones; 25 si implica abusar del aliasing estricto. (Sujeto a que alguien me muestre cómo enviar generosidad a otra persona).


¿Es posible escribir código que se comporte de manera diferente según el nivel de optimización con el que se compiló?

Solo si activa un error del compilador.

EDITAR

Este ejemplo se comporta de manera diferente en gcc 4.5.2:

void foo(int i) { foo(i+1); } main() { foo(0); }

Compilado con -O0 crea un bloqueo de programa con un error de segmentación.
Compilado con -O2 crea un programa que ingresa un bucle sin fin.


Como las llamadas al constructor de copias pueden optimizarse, incluso si tienen efectos secundarios, tener constructores de copia con efectos secundarios hará que el código no optimizado y optimizado se comporte de manera diferente.


Cualquier comportamiento indefinido de acuerdo con el estándar puede cambiar su comportamiento según el nivel de optimización (o fase lunar, para el caso).


Este programa C invoca un comportamiento indefinido, pero muestra resultados diferentes en diferentes niveles de optimización:

#include <stdio.h> /* $ for i in 0 1 2 3 4 do echo -n "$i: " && gcc -O$i x.c && ./a.out done 0: 5 1: 5 2: 5 3: -1 4: -1 */ void f(int a) { int b; printf("%d/n", (int)(&a-&b)); } int main() { f(0); return 0; }


La opción -fstrict-aliasing puede provocar fácilmente cambios en el comportamiento si tiene dos punteros al mismo bloque de memoria. Se supone que esto no es válido, pero en realidad es bastante común.


La parte del estándar de C ++ que se aplica es §1.9 "Ejecución del programa". Se lee, en parte:

se requieren implementaciones conformes para emular (solo) el comportamiento observable de la máquina abstracta como se explica a continuación. ...

Una implementación conforme que ejecuta un programa bien formado debe producir el mismo comportamiento observable que una de las posibles secuencias de ejecución de la instancia correspondiente de la máquina abstracta con el mismo programa y la misma entrada. ...

El comportamiento observable de la máquina abstracta es su secuencia de lecturas y escrituras en datos volátiles y llamadas a funciones de E / S de la biblioteca. ...

Entonces, sí, el código puede comportarse de manera diferente en diferentes niveles de optimización, pero (suponiendo que todos los niveles produzcan un compilador conforme), pero no pueden comportarse de forma observable de manera diferente .

EDITAR: Permítame corregir mi conclusión: Sí, el código puede comportarse de manera diferente en diferentes niveles de optimización siempre que cada comportamiento sea observablemente idéntico a uno de los comportamientos de la máquina abstracta del estándar.


Los cálculos de punto flotante son una fuente madura para las diferencias. Dependiendo de cómo se ordenan las operaciones individuales, puede obtener errores de redondeo más o menos.

El código multihilo no seguro también puede tener diferentes resultados dependiendo de cómo se optimicen los accesos a la memoria, pero eso es esencialmente un error en su código de todos modos.

Y como mencionaste, los efectos secundarios en los constructores de copia pueden desaparecer cuando cambian los niveles de optimización.


OK, mi juego flagrante para la recompensa, al proporcionar un ejemplo concreto. Reuniré los fragmentos de las respuestas de otras personas y mis comentarios.

A los efectos de un comportamiento diferente en diferentes niveles de optimización, "nivel de optimización A" denotará gcc -O0 (estoy usando la versión 4.3.4, pero no importa mucho, creo que cualquier versión incluso vagamente reciente mostrará la diferencia Estoy detrás), y "nivel de optimización B" indicará gcc -O0 -fno-elide-constructors .

El código es simple:

#include <iostream> struct Foo { ~Foo() { std::cout << "~Foo/n"; } }; int main() { Foo f = Foo(); }

Salida en el nivel de optimización A:

~Foo

Salida en el nivel de optimización B:

~Foo ~Foo

El código es totalmente legal, pero el resultado depende de la implementación debido a la elisión del constructor de copias y, en particular, es sensible al indicador de optimización de gcc que deshabilita la elisión de copiar el código.

Tenga en cuenta que, en general, la "optimización" se refiere a las transformaciones del compilador que pueden alterar el comportamiento que no está definido, no especificado o definido por la implementación, pero no el comportamiento definido por el estándar. Por lo tanto, cualquier ejemplo que satisfaga sus criterios necesariamente es un programa cuyo resultado no está especificado o está definido por la implementación. En este caso, no se especifica en el estándar si se eliminan las copiadoras, simplemente tengo la suerte de que GCC las elude de manera confiable siempre que se permite, pero tiene la opción de desactivarlas.


Obtuve un ejemplo interesante en mi curso de SO hoy. Analizamos algunos mutex de software que podrían dañarse en la optimización porque el compilador no sabe acerca de la ejecución paralela.

El compilador puede reordenar declaraciones que no operan en datos dependientes. Como ya dije en código paralelizado, esta dependencia está oculta para el compilador, por lo que podría romperse. El ejemplo que proporcioné nos llevaría a tiempos difíciles en la depuración ya que la seguridad del hilo está rota y su código se comporta de manera impredecible debido a problemas de programación del sistema operativo y errores de acceso simultáneo.


Para C, casi todas las operaciones están estrictamente definidas en la máquina abstracta y las optimizaciones solo se permiten si el resultado observable es exactamente el de esa máquina abstracta. Excepciones de esa regla que vienen a la mente:

  • el comportamiento indefinido no tiene que ser consistente entre las diferentes ejecuciones del compilador o las ejecuciones del código defectuoso
  • operaciones de coma flotante pueden causar diferentes redondeos
  • los argumentos para llamar a las funciones se pueden evaluar en cualquier orden
  • expresiones con tipo calificado volatile pueden o no ser evaluadas solo por sus efectos secundarios
  • los literales compuestos calificados const idénticos pueden o no estar doblados en una ubicación de memoria estática