2008-09-16 7 views
12

En la mayoría de los entornos C o C++, hay un modo de "depuración" y una compilación del modo "versión".
Al observar la diferencia entre los dos, se encuentra 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?¿Por qué a menudo un programa C/C++ tiene la optimización desactivada en el modo de depuración?

Respuesta

26

Sin 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ías solo paso a través del código y golpear las líneas 1, 2, 3, 2, 3, 2, 4

Con optimización, es posible que obtenga una ruta de ejecución que se parece 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 al activar la optimización se 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 "funcionalmente" equivalente, en determinadas circunstancias, el comportamiento cambiará.
Aquí es 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 fuera, esto es sencillo, y que poco sabía qué esperar. Sin embargo, si a su vez en la optimización, un par de cosas puede ocurrir:

  • El compilador puede optimizar el bloque, mientras que fuera (que init a 0, nunca será 1)
  • En lugar de acceso a la memoria , acceso puntero puede ser trasladado a un Registro-> no existe actualización de e/S
  • acceso
  • memoria puede almacenar en caché (no necesariamente relacionados con la optimización del compilador)

En todos estos casos, el comportamiento sería drásticamente diferente y más Probablemente mal

+1

¿qué tal 'volátil int * ptr'? IIUC resolverá los puntos 2 y 3, ¿verdad? Sin embargo, no estoy seguro de 'while'. –

4

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 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 de origen debe escribir el valor nuevamente 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 del código (no conozco ningún formato de símbolo que incluya esta información), por lo que no se lo puede mostrar con precisión ya que no sabe 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.

2

¡La expectativa es que se depure la versión de depuración! 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!

1

Si está depurando en el nivel de instrucción más que en el nivel de origen, es mucho más fácil para usted asignar las instrucciones no optimizadas 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 envían con el producto, pero la mayoría de ellos están disponibles en el Microsoft Symbol Server.

3

El código de optimización es un proceso automatizado que mejora el rendimiento del código mientras se 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 escribió 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.

1

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 activadas juntas, si no sabe qué esperar, pensará que el código se está portando mal y volverá a ejecutar la misma instrucció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 JIT al código nativo continúa y solo el código de métodos depurados se convierte transparentemente a una versión no optimizada.

Me gustaría enfatizar que la optimización no debe cambiar el comportamiento del código, a menos que el optimizador utilizado tenga errores, o el código en sí 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.

Código

con símbolos de depuración son más grandes que puede significar más errores de caché, es decir, más lenta, que puede ser un problema para el software de servidor.

Al menos en Linux (y no hay razón por la que Windows debería ser diferente) la información de depuración se empaqueta en una sección separada del archivo 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.

Cuestiones relacionadas