tuto - program c++
¿Por qué un programa C/C++ a menudo tiene la optimización desactivada en modo de depuración? (6)
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 obtener el reordenamiento de las instrucciones, el despliegue de 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 pasar por el código y tocar 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: 2, 3, 3, 4 o incluso solo 4. (La función no hace nada después de todo ...)
En pocas palabras, el código de depuración con la optimización habilitada puede ser un dolor real. Especialmente si tienes funciones grandes.
Tenga en cuenta que activar la optimización cambia el código. En cierto entorno (sistemas críticos de seguridad), esto es inaceptable y el código que se depura debe ser el código enviado. Tengo que depurar con optimización en ese caso.
Si bien el código optimizado y no optimizado debe ser equivalente "funcionalmente", en 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 uno sabe qué esperar. Sin embargo, si activa la optimización, pueden suceder algunas 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 memoria puede estar 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.
En la mayoría de los entornos C o C ++, hay un modo de "depuración" y una compilación de modo de "liberación".
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 "liberar", generalmente tiene todo tipo de optimizaciones activadas.
¿Por qué la diferencia?
La expectativa es que la versión de depuración se depure. Establecer puntos de interrupción, paso único mientras mira variables, seguimientos de pila y todo lo demás que hace en un depurador (IDE u otro) tienen 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 se mezclan con el orden de los códigos de máquina. El desenrollado de bucles es un buen ejemplo. Las subexpresiones comunes pueden levantarse de los bucles. Con la optimización activada, incluso el nivel más simple, puede intentar establecer un punto de interrupción en una línea que, a nivel de código de máquina, no existe. En algún momento no puede controlar una variable local debido a que se mantiene en un registro de la CPU, ¡o incluso se ha optimizado para que no exista!
La optimización del código es un proceso automatizado que mejora el rendimiento del código en tiempo de ejecución y preserva la semántica. Este proceso puede eliminar resultados intermedios que no son necesarios para completar una evaluación de expresión o función, pero pueden ser de su interés cuando se depura. Del mismo modo, las optimizaciones pueden alterar el flujo de control aparente para que las cosas sucedan en un orden ligeramente diferente de lo que aparece en el código fuente. Esto se hace para omitir cálculos innecesarios o redundantes. Esta modificación del código puede interferir con el mapeo entre los números de línea del código fuente y las direcciones del código del objeto, dificultando que un depurador siga el flujo de control tal como lo escribió.
La depuración en modo no optimizado le permite ver todo lo que ha escrito tal como lo ha escrito sin que el optimizador haya eliminado o reordenado 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 en estos días, sigue siendo una buena idea construir 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) en modo optimizado y no optimizado.
Otra diferencia crucial entre depurar y liberar es cómo se almacenan las variables locales. Conceptualmente, las variables locales se asignan al 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, por lo que el depurador puede mostrárselo. El depurador busca en 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 de nuevo en la ubicación correcta en la pila. Esto es muy ineficiente debido a la sobrecarga de memoria.
En una compilación de lanzamiento, el compilador puede asignar una variable local a un registro para una parte de una función. En algunos casos, puede 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 los registros se asignan a las variables locales para un punto particular en el código (no conozco ningún formato de símbolo que incluya esta información), por lo que no puede mostrarlo con precisión, ya que no lo hace. No sé a dónde ir buscando.
Otra optimización sería la función en línea. En compilaciones optimizadas, el compilador puede reemplazar una llamada a foo () con el código real para foo en cualquier lugar que se use porque la función es lo suficientemente pequeña. Sin embargo, cuando intenta establecer un punto de interrupción en foo () el depurador desea saber la dirección de las instrucciones para foo (), y ya no hay una respuesta simple a esto, puede haber miles de copias del foo ( ) bytes de código repartidos en su programa. Una compilación de depuración le garantizará que hay un lugar donde colocar el punto de interrupción.
Si está depurando en el nivel de instrucción en lugar de en el nivel de origen, es mucho más fácil para mapear las instrucciones no optimizadas de nuevo a la fuente. Además, los compiladores ocasionalmente tienen errores en sus optimizadores.
En la división de Windows en Microsoft, todos los archivos binarios de versiones 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 incluyen con el producto, pero la mayoría 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 las pasará por 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á portando mal y volverá a ejecutar la misma declaración varias veces; le ocurrió a algunos de mis colegas. También la información de depuración dada por GCC con optimizaciones tienden a ser de peor calidad de la que podrían, en realidad.
Sin embargo, en idiomas alojados en una máquina virtual como Java, las optimizaciones y la depuración pueden coexistir, incluso durante la depuración, la compilación de JIT con el código nativo continúa y solo el código de métodos depurados se convierte de forma transparente en 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 tenga errores y dependa de una semántica parcialmente indefinida; el ú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 razón por la cual Windows debería ser diferente) la información de depuración se empaqueta en una sección separada del binario y no se carga durante la ejecución normal. Se pueden dividir en un archivo diferente que se utilizará para la depuración. Además, en algunos compiladores (incluido Gcc, supongo que también con el compilador de C de Microsoft) la información de depuración y las optimizaciones se pueden habilitar juntas. Si no, obviamente el código va a ser más lento.