¿El nivel de optimización -O3 es peligroso en g ++?

233

He escuchado de varias fuentes (aunque principalmente de un colega mío), que compilar con un nivel de optimización de -O3g ++ es de alguna manera 'peligroso', y debe evitarse en general a menos que se demuestre que es necesario.

¿Es esto cierto? Y si es así, ¿por qué? ¿Debería seguir -O2?

Dunnie
fuente
38
Solo es peligroso si confías en un comportamiento indefinido. E incluso entonces me sorprendería si fue el nivel de optimización lo que arruinó algo.
Seth Carnegie
55
El compilador todavía está obligado a producir un programa que se comporte "como si" compilara su código exactamente. No sé que -O3se considera particularmente con errores? Creo que quizás pueda "empeorar" el comportamiento indefinido, ya que puede hacer cosas raras y maravillosas basadas en ciertas suposiciones, pero eso sería culpa tuya. En general, diría que está bien.
BoBTFish
55
Es cierto que los niveles de optimización más altos son más propensos a errores de compilación. He tocado algunos casos, pero en general siguen siendo bastante raros.
Mysticial
21
-O2se enciende -fstrict-aliasing, y si su código sobrevive, entonces probablemente sobrevivirá a otras optimizaciones, ya que esa es una de las personas que se equivocan una y otra vez. Dicho esto, -fpredictive-commoningsolo está -O3habilitado, y habilitarlo podría habilitar errores en su código causados ​​por suposiciones incorrectas sobre concurrencia. Cuanto menos incorrecto sea su código, menos peligrosa es la optimización ;-)
Steve Jessop
66
@PlasmaHH, no creo que "más estricto" sea una buena descripción -Ofast, apaga el manejo de NaNs que cumple con IEEE, por ejemplo
Jonathan Wakely

Respuestas:

223

En los primeros días de gcc (2.8, etc.) y en los tiempos de egcs, y redhat 2.96 -O3 era bastante defectuoso a veces. Pero esto fue hace más de una década, y -O3 no es muy diferente a otros niveles de optimizaciones (en errores).

Sin embargo, tiende a revelar casos en los que las personas confían en un comportamiento indefinido, debido a que se basan más estrictamente en las reglas, y especialmente en los casos esquimales, de los idiomas.

Como nota personal, llevo muchos años ejecutando software de producción en el sector financiero con -O3 y todavía no he encontrado un error que no hubiera estado allí si hubiera usado -O2.

Por demanda popular, aquí una adición:

-O3 y especialmente indicadores adicionales como -funroll-loops (no habilitado por -O3) a veces pueden generar más código de máquina. Bajo ciertas circunstancias (por ejemplo, en una CPU con caché de instrucciones L1 excepcionalmente pequeña) esto puede causar una desaceleración debido a todo el código de, por ejemplo, algún bucle interno que ahora ya no se ajusta a L1I. En general, gcc se esfuerza por no generar tanto código, pero como generalmente optimiza el caso genérico, esto puede suceder. Las opciones especialmente propensas a esto (como el desenrollado de bucle) normalmente no se incluyen en -O3 y se marcan en consecuencia en la página de manual. Como tal, generalmente es una buena idea usar -O3 para generar código rápido, y solo recurrir a -O2 u -Os (que intenta optimizar el tamaño del código) cuando sea apropiado (por ejemplo, cuando un generador de perfiles indica que L1I falla).

Si desea llevar la optimización al extremo, puede modificar gcc a través de --param los costos asociados con ciertas optimizaciones. Además, tenga en cuenta que gcc ahora tiene la capacidad de poner atributos en las funciones que controlan la configuración de optimización solo para estas funciones, por lo que cuando encuentre que tiene un problema con -O3 en una función (o si desea probar indicadores especiales solo para esa función), no necesita compilar todo el archivo o incluso todo el proyecto con O2.

otoh parece que se debe tener cuidado al usar -Ofast, que establece:

-Ofast habilita todas las optimizaciones de -O3. También permite optimizaciones que no son válidas para todos los programas compatibles estándar.

lo que me hace concluir que -O3 está destinado a cumplir totalmente con los estándares.

PlasmaHH
fuente
2
Solo uso algo como lo contrario. Siempre uso -Os -O2 (a veces, O2 genera un ejecutable más pequeño). Después de la creación de perfiles, uso O3 en partes del código que requieren más tiempo de ejecución y eso solo puede dar hasta un 20% más de velocidad.
CoffeDeveloper
3
Lo hago por la velocidad. O3 la mayoría de las veces hace las cosas más lentas. No sé exactamente por qué, sospecho que contamina el caché de instrucciones.
CoffeDeveloper
44
@DarioOO Siento que alegar que "code bloat" es algo popular, pero casi nunca lo veo respaldado con puntos de referencia. Depende mucho de la arquitectura, pero cada vez que veo puntos de referencia publicados (por ejemplo, phoronix.com/… ) muestra que O3 es más rápido en la gran mayoría de los casos. He visto el análisis detallado y cuidadoso requerido para demostrar que la hinchazón de código era realmente un problema, y ​​generalmente solo sucede para las personas que adoptan plantillas de una manera extrema.
Nir Friedman
1
@NirFriedman: tiende a tener un problema cuando el modelo de costo de alineación del compilador tiene errores, o cuando optimizas para un objetivo totalmente diferente del que ejecutas. Interesantemente esto se aplica a todos los niveles de optimización ...
PlasmaHH
1
@PlasmaHH: el problema de usar-cmov sería difícil de solucionar para el caso general. Por lo general, no acaba de ordenar sus datos, por lo que cuando gcc intenta decidir si una rama es predecible o no, std::sortes poco probable que el análisis estático que busca llamadas a funciones ayude. Usar algo como stackoverflow.com/questions/109710/… ayudaría, o tal vez escribir la fuente para aprovechar la ordenación: escanee hasta que vea> = 128, luego comience a sumar. En cuanto al código hinchado, sí, tengo la intención de informarlo. : P
Peter Cordes
42

En mi experiencia un tanto a cuadros, aplicar -O3a un programa completo casi siempre lo hace más lento (en relación con -O2), ya que activa el desenrollado y la línea de bucle agresivo que hacen que el programa ya no encaje en el caché de instrucciones. Para programas más grandes, esto también puede ser cierto en -O2relación con -Os!

El patrón de uso previsto para -O3es, después de perfilar su programa, lo aplica manualmente a un pequeño puñado de archivos que contienen bucles internos críticos que realmente se benefician de estos intercambios agresivos de espacio por velocidad. Las versiones más nuevas de GCC tienen un modo de optimización guiado por perfil que puede (IIUC) aplicar selectivamente las -O3optimizaciones a funciones activas, automatizando efectivamente este proceso.

zwol
fuente
10
"casi siempre"? Hazlo "50-50", y tendremos un trato ;-).
No-Bugs Hare
12

La opción -O3 activa optimizaciones más costosas, como la función en línea, además de todas las optimizaciones de los niveles inferiores '-O2' y '-O1'. El nivel de optimización '-O3' puede aumentar la velocidad del ejecutable resultante, pero también puede aumentar su tamaño. En algunas circunstancias donde estas optimizaciones no son favorables, esta opción podría hacer que un programa sea más lento.

Neel
fuente
3
Entiendo que algunas "optimizaciones aparentes" pueden hacer que un programa sea más lento, pero ¿tiene una fuente que afirme que GCC -O3 ha hecho un programa más lento?
Mooing Duck
1
@MooingDuck: Si bien no puedo citar una fuente, recuerdo haber encontrado un caso así con algunos procesadores AMD más antiguos que tenían un caché L1I bastante pequeño (~ 10k instrucciones). Estoy seguro de que Google tiene más para los interesados, pero especialmente las opciones como el desenrollado de bucles no son parte de O3, y aumentan mucho los tamaños. -Os es para cuando quieres que el ejecutable sea el más pequeño. Incluso -O2 puede aumentar el tamaño del código. Una buena herramienta para jugar con el resultado de diferentes niveles de optimización es el explorador gcc.
PlasmaHH
@PlasmaHH: En realidad, un pequeño tamaño de caché es algo que un compilador podría arruinar, buen punto. Ese es un muy buen ejemplo. Por favor póngalo en la respuesta.
Mooing Duck
1
@PlasmaHH Pentium III tenía 16 KB de caché de código. El K6 de AMD y superiores en realidad tenían 32 KB de caché de instrucciones. P4 comenzó con un valor de alrededor de 96 KB. Core I7 en realidad tiene un caché de código L1 de 32 KB. Los decodificadores de instrucciones son fuertes hoy en día, por lo que su L3 es lo suficientemente bueno como para recurrir a casi cualquier bucle.
doug65536
1
Verá un aumento enorme en el rendimiento cada vez que haya una función llamada en un bucle y pueda hacer una eliminación de subexpresión común significativa e izar el recálculo innecesario de la función antes del bucle.
doug65536
8

Sí, O3 tiene errores. Soy un desarrollador de compiladores e identifiqué errores claros y obvios de gcc causados ​​por O3 que genera instrucciones de ensamblaje SIMD con errores al construir mi propio software. Por lo que he visto, la mayoría del software de producción viene con O2, lo que significa que O3 obtendrá menos atención en las pruebas de errores y correcciones de errores.

Piénselo de esta manera: O3 agrega más transformaciones sobre O2, lo que agrega más transformaciones sobre O1. Estadísticamente hablando, más transformaciones significa más errores. Eso es cierto para cualquier compilador.

David Yeager
fuente
3

Recientemente tuve un problema al usar la optimización con g++. El problema estaba relacionado con una tarjeta PCI, donde los registros (para comando y datos) estaban representados por una dirección de memoria. Mi controlador asignó la dirección física a un puntero dentro de la aplicación y se la dio al proceso llamado, que funcionó así:

unsigned int * pciMemory;
askDriverForMapping( & pciMemory );
...
pciMemory[ 0 ] = someCommandIdx;
pciMemory[ 0 ] = someCommandLength;
for ( int i = 0; i < sizeof( someCommand ); i++ )
    pciMemory[ 0 ] = someCommand[ i ];

La tarjeta no actuó como se esperaba. Cuando vi la asamblea entendí que el compilador solo escribía someCommand[ the last ]enpciMemory , omitiendo todas las escrituras anteriores.

En conclusión: sea preciso y atento con la optimización.

borisbn
fuente
38
Pero el punto aquí es que su programa simplemente tiene un comportamiento indefinido; El optimizador no hizo nada malo. En particular, debe declarar pciMemorycomo volatile.
Konrad Rudolph
11
En realidad no es UB, pero el compilador tiene derecho a omitir todas las escrituras, excepto las últimas, pciMemoryporque todas las demás escrituras probablemente no tengan efecto. Para el optimizador es increíble porque puede eliminar muchas instrucciones inútiles y que consumen mucho tiempo.
Konrad Rudolph el
44
Encontré esto en estándar (después de más de 10 años))): se puede usar una declaración volátil para describir un objeto correspondiente a un puerto de entrada / salida mapeado en memoria o un objeto al que se accede mediante una función de interrupción asíncrona. Las acciones sobre los objetos así declarados no se '' optimizarán '' por una implementación o se reordenarán, excepto según lo permitan las reglas para evaluar expresiones.
borisbn
2
@borisbn Algo fuera de tema, pero ¿cómo sabes que tu dispositivo ha tomado el comando antes de enviar un nuevo comando?
user877329
3
@ user877329 Lo vi por el comportamiento del dispositivo, pero fue una gran búsqueda
borisbn