¿Por qué un programa C/C++ a menudo tiene la optimización desactivada en modo de depuración?

StackOverflow https://stackoverflow.com/questions/69250

  •  09-06-2019
  •  | 
  •  

Pregunta

En la mayoría de los entornos C o C++, existe un modo de "depuración" y un modo de compilación de "lanzamiento".
Al observar la diferencia entre los dos, encontrará que el modo de depuración agrega los símbolos de depuración (a menudo la opción -g en muchos compiladores) pero también deshabilita la mayoría de las optimizaciones.
En el modo "lanzamiento", normalmente tienes activado todo tipo de optimizaciones.
¿Por qué la diferencia?

¿Fue útil?

Solución

Sin ninguna optimización, el flujo a través de su código es lineal.Si está en la línea 5 y en un solo paso, pasa a la línea 6.Con la optimización activada, puede reordenar instrucciones, desenrollar bucles y todo tipo de optimizaciones.
Por ejemplo:


void foo() {
1:  int i;
2:  for(i = 0; i < 2; )
3:    i++;
4:  return;

En este ejemplo, sin optimización, podría recorrer el código de una sola vez y presionar las líneas 1, 2, 3, 2, 3, 2, 4.

Con la optimización activada, es posible que obtenga una ruta de ejecución similar a la siguiente:¡2, 3, 3, 4 o incluso solo 4!(La función no hace nada después de todo...)

En pocas palabras, ¡depurar código con la optimización habilitada puede ser un verdadero dolor de cabeza!Especialmente si tienes funciones grandes.

Tenga en cuenta que activar la optimización cambia el código.En determinados entornos (sistemas críticos para la seguridad), esto es inaceptable y el código que se está depurando debe ser el código enviado.En ese caso, tengo que depurar con la optimización activada.

Si bien el código optimizado y no optimizado debe ser "funcionalmente" equivalente, bajo determinadas circunstancias, el comportamiento cambiará.
Aquí hay un ejemplo simplista:

    int* ptr = 0xdeadbeef;  // some address to memory-mapped I/O device
    *ptr = 0;   // setup hardware device
    while(*ptr == 1) {    // loop until hardware device is done
       // do something
    }

Con la optimización desactivada, esto es sencillo y sabes qué esperar.Sin embargo, si activa la optimización, pueden suceder un par de cosas:

  • El compilador podría optimizar el bloque while (iniciamos en 0, nunca será 1)
  • En lugar de acceder a la memoria, el acceso al puntero podría moverse a un registro->Sin actualización de E/S
  • el acceso a la memoria puede estar almacenado en caché (no necesariamente relacionado con la optimización del compilador)

En todos estos casos, el comportamiento sería drásticamente diferente y muy probablemente incorrecto.

Otros consejos

Otra diferencia crucial entre depurar y liberar es cómo se almacenan las variables locales.Conceptualmente, a las variables locales se les asigna almacenamiento en un marco de pila de funciones.El archivo de símbolos generado por el compilador le dice al depurador el desplazamiento de la variable en el marco de la pila, para que el depurador pueda mostrárselo.El depurador echa un vistazo a la ubicación de la memoria para hacer esto.

Sin embargo, esto significa que cada vez que se cambia una variable local, el código generado para esa línea fuente debe escribir el valor nuevamente en la ubicación correcta de la pila.Esto es muy ineficiente debido a la sobrecarga de memoria.

En una versión de lanzamiento, el compilador puede asignar una variable local a un registro para una parte de una función.En algunos casos, es posible que no le asigne ningún almacenamiento de pila (cuantos más registros tenga una máquina, más fácil será hacerlo).

Sin embargo, el depurador no sabe cómo se asignan los registros a las variables locales para un punto particular del código (no conozco ningún formato de símbolo que incluya esta información), por lo que no puede mostrárselo con precisión, ya que no No sé dónde ir a buscarlo.

Otra optimización sería la función incorporada.En compilaciones optimizadas, el compilador puede reemplazar una llamada a foo() con el código real de foo en todos los lugares donde se usa porque la función es lo suficientemente pequeña.Sin embargo, cuando intenta establecer un punto de interrupción en foo(), el depurador quiere saber la dirección de las instrucciones para foo(), y ya no hay una respuesta simple para esto: puede haber miles de copias de foo( ) bytes de código repartidos por su programa.Una compilación de depuración garantizará que haya un lugar donde colocar el punto de interrupción.

La optimización del código es un proceso automatizado que mejora el rendimiento en tiempo de ejecución del código y al mismo tiempo preserva la semántica.Este proceso puede eliminar resultados intermedios que son innecesarios para completar una expresión o evaluación de función, pero que pueden ser de su interés al realizar la depuración.De manera similar, las optimizaciones pueden alterar el flujo de control aparente para que las cosas sucedan en un orden ligeramente diferente al que aparece en el código fuente.Esto se hace para omitir cálculos innecesarios o redundantes.Esta reajustación del código puede alterar la asignación entre los números de línea del código fuente y las direcciones del código objeto, lo que dificulta que un depurador siga el flujo de control mientras lo escribía.

La depuración en modo no optimizado le permite ver todo lo que ha escrito tal como lo escribió sin que el optimizador elimine o reordene las cosas.

Una vez que esté satisfecho de que su programa esté funcionando correctamente, puede activar las optimizaciones para obtener un mejor rendimiento.Aunque los optimizadores son bastante confiables hoy en día, sigue siendo una buena idea crear un conjunto de pruebas de buena calidad para garantizar que su programa se ejecute de manera idéntica (desde un punto de vista funcional, sin considerar el rendimiento) tanto en modo optimizado como en modo no optimizado.

La expectativa es que la versión de depuración esté depurada.Establecer puntos de interrupción, realizar un solo paso mientras se observan variables, seguimientos de pila y todo lo demás que se hace en un depurador (IDE o de otro tipo) tiene sentido si cada línea de código fuente no vacío y sin comentarios coincide con alguna instrucción de código de máquina.

La mayoría de las optimizaciones alteran el orden de los códigos de máquina.El desenrollado de bucles es un buen ejemplo.Las subexpresiones comunes se pueden eliminar de los bucles.Con la optimización activada, incluso en el nivel más simple, es posible que esté intentando establecer un punto de interrupción en una línea que, a nivel de código de máquina, no existe.A veces no se puede monitorear una variable local debido a que se mantiene en un registro de la CPU, o tal vez incluso se optimiza para que no exista.

Si está depurando en el nivel de instrucción en lugar de en el nivel de fuente, le resultará muchísimo más fácil asignar instrucciones no optimizadas a la fuente.Además, los compiladores ocasionalmente tienen errores en sus optimizadores.

En la división de Windows de Microsoft, todos los binarios de lanzamiento se crean con símbolos de depuración y optimizaciones completas.Los símbolos se almacenan en archivos PDB separados y no afectan el rendimiento del código.No se envían con el producto, pero la mayoría de ellos están disponibles en el Servidor de símbolos de Microsoft.

Otro de los problemas con las optimizaciones son las funciones en línea, también en el sentido de que siempre podrás recorrerlas en un solo paso.

Con GCC, con la depuración y las optimizaciones habilitadas juntas, si no sabe qué esperar, pensará que el código se está comportando mal y vuelve a ejecutar la misma declaración varias veces; les pasó a un par de mis colegas.Además, la información de depuración proporcionada por GCC con optimizaciones tiende a ser de peor calidad de lo que podría, en realidad.

Sin embargo, en lenguajes alojados en una máquina virtual como Java, las optimizaciones y la depuración pueden coexistir; incluso durante la depuración, la compilación JIT en código nativo continúa y solo el código de los métodos depurados se convierte de forma transparente a una versión no optimizada.

Me gustaría enfatizar que la optimización no debería cambiar el comportamiento del código, a menos que el optimizador utilizado tenga errores, o que el código en sí tenga errores y dependa de una semántica parcialmente indefinida;este último es más común en la programación multiproceso o cuando también se utiliza el ensamblaje en línea.

El código con símbolos de depuración es más grande, lo que puede significar más errores de caché, es decir,más lento, lo que puede ser un problema para el software del servidor.

Al menos en Linux (y no hay ninguna razón por la que Windows deba ser diferente), la información de depuración está empaquetada en una sección separada del binario y no se carga durante la ejecución normal.Se pueden dividir en un archivo diferente para usarlo en la depuración.Además, en algunos compiladores (incluido Gcc, supongo que también con el compilador C de Microsoft), la información de depuración y las optimizaciones se pueden habilitar juntas.Si no, obviamente el código será más lento.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top