¿Cómo sé si el compilador rompió mi código y qué hago si fue el compilador?

14

De vez en cuando, el código C ++ no funcionará cuando se compila con algún nivel de optimización. Puede ser que el compilador haga una optimización que rompa el código o puede ser un código que contenga un comportamiento indefinido que permita al compilador hacer lo que sienta.

Supongamos que tengo un fragmento de código que se rompe cuando se compila solo con un nivel de optimización más alto. ¿Cómo sé si es el código o el compilador y qué hago si es el compilador?

diente filoso
fuente
43
Lo más probable es que seas tú.
littleadv
99
@littleadv, incluso las versiones recientes de gcc y msvc están llenas de errores, por lo que no estaría tan seguro.
SK-logic
3
¿Tienes todas las advertencias habilitadas?
@ Thorbjørn Ravn Andersen: Sí, los tengo habilitados.
Sharptooth
3
FWIW: 1) Trato de no hacer nada complicado que pueda tentar al compilador a equivocarse, 2) el único lugar donde importan las banderas de optimización (para la velocidad) es en el código donde el contador del programa pasa una fracción significativa de su tiempo. A menos que esté escribiendo bucles de CPU ajustados, en muchas aplicaciones la PC pasa esencialmente todo su tiempo en bibliotecas o en E / S. En ese tipo de aplicación, los modificadores / O no te ayudan en absoluto.
Mike Dunlavey

Respuestas:

19

Diría que es una apuesta segura que, en la gran mayoría de los casos, es su código, no el compilador, el que está roto. E incluso en el caso extraordinario cuando se trata del compilador, probablemente esté utilizando alguna característica de lenguaje oscuro de una manera inusual, para la cual el compilador específico no está preparado; en otras palabras, lo más probable es que cambie su código para que sea más idiomático y evite el punto débil del compilador.

En cualquier caso, si puede demostrar que encontró un error del compilador (según las especificaciones del idioma), informe a los desarrolladores del compilador, para que puedan solucionarlo en algún momento.

Péter Török
fuente
@ SK-logic, bastante justo, no tengo estadísticas que lo respalden. Se basa en mi propia experiencia, y admito que rara vez he ampliado los límites del lenguaje y / o del compilador; otros pueden hacerlo más a menudo.
Péter Török
(1) @ SK-Logic: Acabo de encontrar un error del compilador de C ++, el mismo código, probé en un compilador y funciona, probé en otro.
umlcat
8
@umlcat: lo más probable es que sea su código dependiendo del comportamiento no especificado; en un compilador coincide con sus expectativas, en otro no. eso no significa que esté roto.
Javier
@Ritch Melton, ¿alguna vez has usado LTO?
SK-logic
1
Estoy de acuerdo con Crashworks, cuando hablamos de consolas de juegos. No es inusual encontrar errores de compilación esotéricos en esa situación específica. Sin embargo, si está apuntando a PC normales, utilizando un compilador muy utilizado, entonces es muy poco probable que se encuentre con un error del compilador que nadie ha visto antes.
Trevor Powell
14

Igual que de costumbre, como con cualquier otro error: realiza un experimento controlado. Limite el área sospechosa, desactive las optimizaciones para todo lo demás y comience a variar las optimizaciones aplicadas a ese fragmento de código. Una vez que obtenga una reproducibilidad del 100%, comience a variar su código e introduzca cosas que podrían romper ciertas optimizaciones (por ejemplo, introducir un posible alias de puntero, insertar llamadas externas con posibles efectos secundarios, etc.). Mirar el código de ensamblaje en un depurador también podría ayudar.

SK-logic
fuente
podría ayudar con qué? Si es un error del compilador, ¿y qué?
littleadv
2
@littleadv, si se trata de un error del compilador, puede intentar solucionarlo (o simplemente informarlo correctamente, con todo detalle), o puede averiguar cómo evitarlo en el futuro, si está condenado a seguir usando esto versión de tu compilador por un tiempo. Si se trata de algo con su propio código, uno de los numerosos problemas límite de C ++, este tipo de escrutinio también ayuda a corregir un error y evitar su tipo en el futuro.
SK-logic
Entonces, como dije en mi respuesta, aparte de informar, no hay mucha diferencia en el tratamiento, independientemente de quién sea la culpa.
littleadv
3
@littleadv, sin comprender la naturaleza de un problema, es probable que lo enfrentes una y otra vez. Y a menudo existe la posibilidad de arreglar un compilador por su cuenta. Y, sí, desafortunadamente no es "improbable" encontrar un error en un compilador de C ++.
SK-logic
10

Examine el código de ensamblaje resultante y vea si hace lo que su fuente está pidiendo. Recuerde que las probabilidades son muy altas de que realmente es su código el culpable de una manera no obvia.

Loren Pechtel
fuente
1
Esta es realmente la única respuesta a esta pregunta. El trabajo de los compiladores, en este caso, es llevarlo de C ++ al lenguaje ensamblador. Crees que es el compilador ... verifica el funcionamiento de los compiladores. Es así de simple.
old_timer
7

En más de 30 años de programación, el número de errores genuinos del compilador (generación de código) que he encontrado sigue siendo solo ~ 10. El número de errores propios (y de otras personas) que he encontrado y corregido en el mismo período es probablemente > 10,000. Mi "regla de oro" es que la probabilidad de que un error determinado se deba al compilador es <0.001.

Paul R
fuente
1
Tienes suerte. Mi promedio es de aproximadamente 1 error realmente malo al mes, y los problemas menores de límite son mucho más frecuentes. Y cuanto mayor sea el nivel de optimización que esté utilizando, mayores serán las posibilidades de error del compilador. Si está tratando de usar -O3 y LTO, sería muy afortunado de no encontrar un par de ellos en poco tiempo. Y solo cuento aquí los errores en las versiones de lanzamiento: como desarrollador de compiladores, me enfrento a muchos más problemas de ese tipo en mi trabajo, pero eso no cuenta. Solo sé lo fácil que es arruinar un compilador.
SK-logic
2
25 años y también he visto muchos. Los compiladores empeoran cada año.
old_timer
5

Comencé a escribir un comentario y luego decidí que era demasiado largo y demasiado al grano.

Yo diría que es su código el que está roto. En el caso poco probable de que haya descubierto un error en el compilador, debe informarlo a los desarrolladores del compilador, pero ahí es donde termina la diferencia.

La solución es identificar la construcción ofensiva y refactorizarla para que haga la misma lógica de manera diferente. Eso probablemente resolvería el problema, ya sea que el error esté de su lado o en el compilador.

littleadv
fuente
5
  1. Vuelva a leer su código a fondo. Asegúrese de no hacer cosas con efectos secundarios en ASSERT, u otras declaraciones específicas de depuración (o más generales, de configuración). Recuerde también que en una compilación de depuración, la memoria se inicializa de manera diferente: los valores indicadores del puntero se pueden verificar aquí: Depuración: representaciones de asignación de memoria . Cuando se ejecuta desde Visual Studio, casi siempre usa Debug Heap (incluso en modo de lanzamiento) a menos que especifique explícitamente con una variable de entorno que esto no es lo que desea.
  2. Comprueba tu construcción. Es común tener problemas con compilaciones complejas en otros lugares que no sean el compilador real; las dependencias a menudo son las culpables. Sé que "has intentado reconstruir completamente" es una respuesta casi tan irritante como "has intentado reinstalar Windows", pero a menudo ayuda. Intente: a) Reiniciar. b) Eliminar todos los archivos intermedios y de salida MANUALMENTE y reconstruirlos.
  3. Revise su código para verificar las posibles ubicaciones en las que podría estar invocando un comportamiento indefinido. Si has estado trabajando en C ++ por un tiempo, sabrás que hay algunos puntos en los que piensas "No estoy completamente seguro de que puedo asumir que ..." - búscalo en Google o pregunta aquí sobre ese particular tipo de código para ver si es un comportamiento indefinido o no.
  4. Si ese no parece ser el caso, genere una salida preprocesada para el archivo que está causando los problemas. Una expansión de macro inesperada puede causar todo tipo de diversión (recuerdo el momento en que un colega decidió que una macro con el nombre de H sería una buena idea ...). Examine la salida preprocesada para detectar cambios inesperados entre las configuraciones de su proyecto.
  5. Último recurso, ahora realmente está en la tierra de errores del compilador, mire la salida del ensamblaje. Esto podría requerir un poco de investigación y lucha solo para tener una idea de lo que el ensamblaje realmente está haciendo, pero en realidad es bastante informativo. También puede usar las habilidades que adquiere aquí para evaluar las microoptimizaciones, para que no todo esté perdido.
Joris Timmermans
fuente
+1 para "comportamiento indefinido". He sido mordido por ese. Escribió algún código que dependía del int + intdesbordamiento exactamente como si se compilara en una instrucción ADD de hardware. Funcionó bien cuando se compiló con una versión anterior de GCC, pero no cuando se compiló con el compilador más nuevo. Aparentemente, la buena gente de GCC decidió que, dado que el resultado de un desbordamiento de enteros no está definido, su optimizador podría funcionar bajo el supuesto de que nunca sucede. Optimizó una rama importante directamente del código.
Solomon Slow
2

Si desea saber si es su código o el compilador, debe conocer perfectamente la especificación de C ++.

Si la duda persiste, debe conocer perfectamente el ensamblaje x86.

Si no está de humor para aprender ambos a la perfección, entonces es casi seguro que su compilador resuelve de manera diferente dependiendo del nivel de optimización.

Mouviciel
fuente
(+1) @mouviciel: también depende si la función es compatible con el compilador, incluso si está en la especificación. Tengo un error extraño con gcc. Declaro una "estructura c simple" con un "puntero de función", está permitido en la especificación, pero funciona en algunas situaciones, y no en otras.
umlcat
1

Obtener un error de compilación en el código estándar o un error de compilación interno es más probable que los optimizadores estén equivocados. Pero he oído hablar de compiladores que optimizan los bucles incorrectamente olvidando algunos efectos secundarios que causa un método.

No tengo sugerencias sobre cómo saber si eres tú o el compilador. Puedes probar con otro compilador.

Un día me preguntaba si era mi código o no y alguien me sugirió valgrind. Pasé los 5 o 10 minutos para ejecutar mi programa con él (creovalgrind --leak-check=yes myprog arg1 arg2 que lo hice pero jugué con otras opciones) e inmediatamente me mostró UNA línea que se ejecuta en un caso específico, que era el problema. Luego, mi aplicación funcionó sin problemas desde entonces sin fallas, errores o comportamientos extraños. valgrind u otra herramienta como esta es una buena manera de saber si es su código.

Nota al margen: una vez me pregunté por qué el rendimiento de mi aplicación apestaba. Resultó que todos mis problemas de rendimiento también estaban en una línea. Yo escribi for(int i=0; i<strlen(sz); ++i) {. El sz fue unos pocos mb. Por alguna razón, el compilador se ejecutaba cada vez, incluso después de la optimización. Una línea puede ser un gran problema. De actuaciones a choques


fuente
1

Una situación cada vez más común es que los compiladores rompen el código escrito para dialectos de C que soporta comportamientos no obligatorios por el Estándar, y permite que el código dirigido a esos dialectos sea más eficiente de lo que podría ser un código estrictamente conforme. En tal caso, sería injusto describir como código "roto" que sería 100% confiable en los compiladores que implementaron el dialecto de destino, o describir como "roto" el compilador que procesa un dialecto que no admite la semántica requerida . En cambio, los problemas surgen simplemente del hecho de que el lenguaje procesado por los compiladores modernos con optimizaciones habilitadas está divergiendo de los dialectos que solían ser populares (y todavía son procesados ​​por muchos compiladores con optimizaciones deshabilitadas, o por algunos incluso con optimizaciones habilitadas).

Por ejemplo, se escribe una gran cantidad de código para dialectos que reconocen como legítimos una serie de patrones de alias de puntero no obligatorios por la interpretación de gcc del Estándar, y hace uso de dichos patrones para permitir que una traducción directa del código sea más legible y eficiente de lo que sería posible bajo la interpretación de gcc del Estándar C. Tal código puede no ser compatible con gcc, pero eso no implica que esté roto. Simplemente se basa en extensiones que gcc solo admite con optimizaciones deshabilitadas.

Super gato
fuente
Bueno, seguro que no hay nada de malo en codificar C estándar X + extensiones Y y Z, siempre y cuando eso le brinde ventajas significativas, sabe que lo hizo y lo documentó a fondo. Lamentablemente, las tres condiciones normalmente no se cumplen y, por lo tanto, es justo decir que el código está roto.
Deduplicador
@Deduplicator: los compiladores C89 se promocionaron como compatibles con sus predecesores, y también para C99, etc. Mientras que C89 no impone requisitos sobre comportamientos que se habían definido previamente en algunas plataformas pero no en otras, la compatibilidad ascendente sugeriría que los compiladores C89 para las plataformas que habían considerado los comportamientos como se definieron deberían continuar haciéndolo; La justificación para la promoción de tipos cortos sin signo a signos sugeriría que los autores de la Norma esperaban que los compiladores se comportaran de esa manera, tanto si la Norma lo ordenaba como si no. Además ...
supercat
... la interpretación estricta de las reglas de alias arrojaría compatibilidad hacia arriba por la ventana y haría que muchos tipos de código no funcionaran, pero algunos pequeños ajustes (por ejemplo, identificar algunos patrones en los que debería esperarse alias de tipo cruzado y, por lo tanto, permisible) resolvería ambos problemas . Todo el propósito declarado de la regla era evitar que los compiladores hicieran suposiciones de alias "pesimistas", pero dado "float x", debería suponerse que "foo ((int *) & x)" podría modificar x incluso si "foo" no No escriba a ningún puntero del tipo 'float * "o" char * "para ser considerado" pesimista "u" obvio "?
supercat
0

Aísle el punto problemático y compare el comportamiento observado con lo que debería suceder de acuerdo con las especificaciones del lenguaje. Definitivamente no es fácil, pero eso es lo que tienes que hacer para saber (y no solo asumir ).

Probablemente no sería tan meticuloso. Más bien, le pediría al foro de soporte del compilador / lista de correo. Si realmente es un error en el compilador, entonces podrían solucionarlo. Probablemente sería mi código de todos modos. Por ejemplo, las especificaciones de idioma con respecto a la visibilidad de la memoria en subprocesos pueden ser bastante intuitivas, y pueden ser evidentes solo cuando se usan algunos indicadores de optimización específicos, en algún hardware específico (!). La especificación podría definir un comportamiento indefinido, por lo que podría funcionar con algún compilador / algunos indicadores, y no funcionaría con otro, etc.

Joonas Pulakka
fuente
0

Lo más probable es que su código tenga un comportamiento indefinido (como explicaron otros, es mucho más probable que tenga errores en su código que en el compilador, incluso si los compiladores de C ++ son tan complejos que sí tienen errores; incluso la especificación de C ++ tiene errores de diseño) . Y UB puede estar aquí incluso si el ejecutable compilado funciona (por mala suerte).

Por lo tanto, debe leer el blog de Lattner Lo que todo programador de C debe saber sobre el comportamiento indefinido (la mayoría se aplica también a C ++ 11).

La herramienta valgrind y las -fsanitize= opciones de instrumentación recientes para GCC (o Clang / LLVM ) también deberían ser útiles. Y, por supuesto, habilite todas las advertencias:g++ -Wall -Wextra

Basile Starynkevitch
fuente