¿Es realmente una buena práctica deshabilitar las optimizaciones durante las fases de desarrollo y depuración?

He leído Programación de microcontroladores PIC de 16 bits en C , y hay esta afirmación en el libro:

Sin embargo, durante las fases de desarrollo y depuración de un proyecto, siempre es una buena práctica deshabilitar todas las optimizaciones, ya que podrían modificar la estructura del código que se está analizando y hacer que la colocación de un solo paso y el punto de interrupción sean problemáticas.

Confieso que estaba un poco confundido. No entendí si el autor dijo eso debido al período de evaluación del C30 o si es realmente una buena práctica.

Me gustaría saber si realmente usas esta práctica y por qué.

Respuestas (12)

Esto es bastante estándar en la ingeniería de software en general: cuando optimiza el código, el compilador puede reorganizar las cosas como quiera, siempre que no pueda notar ninguna diferencia en la operación. Entonces, por ejemplo, si inicializa una variable dentro de cada iteración de un ciclo y nunca cambia la variable dentro del ciclo, el optimizador puede mover esa inicialización fuera del ciclo, para que no pierda tiempo con eso.

También puede darse cuenta de que calcula un número con el que luego no hace nada antes de sobrescribirlo. En ese caso, podría eliminar el cálculo inútil.

El problema con la optimización es que querrá poner un punto de interrupción en algún fragmento de código que el optimizador haya movido o eliminado. En ese caso, el depurador no puede hacer lo que usted quiere (generalmente, colocará el punto de interrupción en algún lugar cercano). Por lo tanto, para hacer que el código generado se asemeje más a lo que escribió, desactive las optimizaciones durante la depuración; esto asegura que el código que desea romper realmente esté allí.

Sin embargo, debe tener cuidado con esto, ya que, dependiendo de su código, ¡la optimización puede romper cosas! En general, el código que es roto por un optimizador que funciona correctamente es realmente un código con errores que se está saliendo con la suya, por lo que generalmente desea averiguar por qué el optimizador lo rompe.

El contrapunto a este argumento es que el optimizador probablemente hará las cosas más pequeñas y/o más rápidas, y si tiene un código con restricciones de tiempo o tamaño, puede romper algo al deshabilitar la optimización y perder el tiempo depurando un problema que no realmente existo Por supuesto, el depurador también puede hacer que su código sea más lento y más grande.
¿Dónde puedo aprender más sobre esto?
No he trabajado con el compilador C30, pero para el compilador C18 había una nota/manual de la aplicación para el compilador que cubría las optimizaciones que admitía.
@O Engenheiro: consulte los documentos de su compilador para ver qué optimizaciones admite. La optimización varía enormemente según el compilador, las bibliotecas y la arquitectura de destino.
Nuevamente, no para el compilador C30, pero gcc publica una LARGA lista de las diversas optimizaciones que se pueden aplicar. También puede usar esta lista para obtener una optimización detallada, en caso de que tenga una estructura de control particular que desee mantener intacta. La lista está aquí: gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html
@Reemrevnivek, deshabilitaría las optimizaciones de un solo archivo que tienen efecto y tiempo general, pero luego usaría el archivo para comparar y probar otros sistemas. Con todas las optimizaciones, usaría el parpadeo del LED y lo mediría con un microscopio para medir los tiempos.
@OEngenheiro, Eche un vistazo a algunos de los libros que Jack Ganslle recomienda si desea aplicarlos a los sistemas integrados. De lo contrario, asumiría que preguntar SO sería una excelente manera de obtener los libros de CS sobre esta práctica.

Le envié esta pregunta a Jack Ganssle y esto es lo que me respondió:

Daniel,

Prefiero depurar usando cualquier optimización que haya en el código publicado. La NASA dice "prueba lo que vuelas, vuela lo que pruebas". En otras palabras, ¡no haga pruebas y luego cambie el código!

Sin embargo, a veces uno tiene que desactivar las optimizaciones para que el depurador funcione. Intento apagarlos solo en los módulos en los que estoy trabajando. Por esa razón, creo en mantener archivos pequeños, digamos unos cientos de líneas de código más o menos.

Todo lo mejor, Jack

Creo que hay una distinción no declarada entre los dos párrafos en esta respuesta. La prueba se refiere a un procedimiento que se supone que demuestra que los productos blandos, firmes y/o duros funcionan correctamente. La depuración es el proceso en el que el código se ejecuta paso a paso a través de instrucciones para ver por qué no funciona [todavía].
Es bueno tener una opción. Entonces, la prueba puede cubrir más variedades/permutaciones con y sin optimización. Cuanta más cobertura, mejor es la prueba
@reemrevnivek, cuando estás depurando, ¿no estás probando también?
@O Engenheiro - No. Solo depuro errores si falla la prueba.

Depende, y esto es generalmente cierto para todas las herramientas, no solo para C30.

Las optimizaciones a menudo eliminan y/o reestructuran el código de varias maneras. Su declaración de cambio puede volver a implementarse con una construcción if/else o, en algunos casos, puede eliminarse por completo. y = x * 16 puede reemplazarse con una serie de desplazamientos a la izquierda, etc. aunque este último tipo de optimización por lo general todavía se puede realizar paso a paso, es principalmente la reestructuración de la declaración de control lo que te atrapa.

Esto puede hacer que sea imposible pasar un depurador a través de su código C porque las estructuras que definió en C ya no existen, fueron reemplazadas o reordenadas por el compilador en algo que el compilador cree que será más rápido o usará menos espacio. También puede hacer que los puntos de interrupción sean imposibles de establecer desde la lista C, ya que es posible que la instrucción que está rompiendo ya no exista. Por ejemplo, puede intentar establecer un punto de interrupción dentro de una declaración if, pero el compilador puede haberlo eliminado. Puede intentar establecer un punto de interrupción dentro de un ciclo while o for, pero el compilador decidió desenrollar ese ciclo para que ya no exista.

Por esta razón, si puede depurar con las optimizaciones desactivadas, suele ser más fácil. Siempre debe volver a realizar la prueba con las optimizaciones activadas. Esta es la única forma en que descubrirá que se perdió algo importante volatiley está causando fallas intermitentes (o alguna otra rareza).

En el caso del desarrollo integrado, debe tener cuidado con las optimizaciones de todos modos. Específicamente en secciones de código que son críticas en el tiempo, por ejemplo, algunas interrupciones. En estos casos, debe codificar los bits críticos en ensamblador o usar directivas del compilador para asegurarse de que estas secciones no estén optimizadas para que sepa que tienen un tiempo de ejecución fijo o un tiempo de ejecución fijo en el peor de los casos.

El otro problema puede ser encajar el código en el uC, es posible que necesite optimizaciones de densidad de código para simplemente encajar su código en el chip. Esta es una de las razones por las que generalmente es una buena idea comenzar con la mayor capacidad de ROM uC en una familia y solo elegir una más pequeña para la fabricación, después de que su código esté bloqueado.

En general, depuraría con cualquier configuración con la que planee lanzar. Si fuera a lanzar código optimizado, depuraría con código optimizado. Si iba a publicar código no optimizado, depuraría con código no optimizado. Hago esto por dos razones. Primero, los optimizadores pueden generar diferencias de tiempo lo suficientemente significativas como para hacer que el producto final se comporte de manera diferente al código no optimizado. En segundo lugar, aunque la mayoría son bastante buenos, los proveedores de compiladores cometen errores y el código optimizado puede producir resultados diferentes a los del código no optimizado. Como resultado, me gusta obtener la mayor cantidad de tiempo de prueba posible con cualquier configuración con la que planee lanzar.

Dicho esto, los optimizadores pueden dificultar la depuración, como se indicó en las respuestas anteriores. Si encuentro una sección particular de código que es difícil de depurar, apagaré temporalmente el optimizador, haré la depuración para que el código funcione, luego volveré a encender el optimizador y probaré una vez más.

Pero ejecutar el código en un solo paso puede ser casi imposible con las optimizaciones activadas. Depure con las optimizaciones desactivadas y ejecute sus pruebas unitarias con el código de versión.

Mi estrategia normal es desarrollar con la optimización final (máximo para el tamaño o la velocidad, según corresponda), pero desactivar la optimización temporalmente si necesito depurar o rastrear algo. Esto reduce el riesgo de que surjan errores como resultado de cambiar los niveles de optimización.

Un modo de falla típico es cuando el aumento de la optimización hace que surjan errores no vistos anteriormente debido a que no ha declarado las variables como volátiles donde sea necesario; esto es esencial para decirle al compilador qué cosas no deben "optimizarse".

ciertamente tiene sentido en el caso de los puntos de interrupción... ya que el compilador puede eliminar muchas declaraciones que en realidad no afectan la memoria.

considera algo como:

int i =0;

for (int j=0; j < 10; j++)
{
 i+=j;
}
return 0;

podría optimizarse por completo (porque inunca se lee). desde el punto de vista de su punto de interrupción, parecería que omitió todo ese código, cuando básicamente ni siquiera estaba allí ... Supongo que es por eso que en las funciones de tipo de suspensión a menudo verá algo como:

for (int j=delay; j != 0; j--)
{
    asm( " nop " );
    asm( " nop " );
}
return 0;

Use cualquier forma con la que vaya a lanzar, depuradores y compilación para depurar, oculte muchos (MUCHOS) errores que no ve hasta que compila para el lanzamiento. Para entonces, es mucho más difícil encontrar esos errores, en comparación con la depuración sobre la marcha. Hace 20 años y nunca tuve un uso para un gdb u otro depurador, no es necesario mirar variables o un solo paso. Cientos a miles de líneas por día. Entonces es posible, no se deje llevar a pensar lo contrario.

La compilación para la depuración y luego la compilación para el lanzamiento puede requerir el doble o más del doble de esfuerzo. Si se encuentra en un aprieto y tiene que usar una herramienta como un depurador, compile para que el depurador resuelva el problema específico y luego vuelva a la operación normal.

Otros problemas también son ciertos, como que el optimizador hace que el código sea más rápido, por lo que para incrustar, en particular, sus cambios de tiempo con las opciones del compilador y eso puede afectar la funcionalidad de su programa, aquí nuevamente use la opción de compilación entregable durante toda la fase. Los compiladores también son programas y tienen errores y los optimizadores cometen errores y algunos no tienen fe en eso. Si ese es el caso, no hay nada de malo en que se compile sin optimización, solo hágalo de esa manera todo el tiempo. La ruta que prefiero es compilar para la optimización, entonces, si sospecho que hay un problema con el compilador, deshabilite la optimización si eso lo soluciona, por lo general, a veces examina la salida del ensamblador para averiguar por qué.

+1 solo para elaborar su buena respuesta: a menudo, la compilación en modo "depuración" rellenará la pila/montón alrededor de las variables con espacio no asignado para mitigar errores de cadena de formato y cancelación más pequeños. Obtendrá más a menudo un buen bloqueo de tiempo de ejecución si compila en el lanzamiento.

Siempre desarrollo código con -O0 (opción gcc para desactivar la optimización). Cuando sienta que estoy en el punto en el que quiero comenzar a dejar que las cosas se dirijan más hacia un lanzamiento, comenzaré con -Os (optimizar para el tamaño) ya que, en general, cuanto más código pueda mantener en caché, mejor será. incluso si no está súper optimizado.

Encuentro que gdb funciona mucho mejor con el código -O0, y es mucho más fácil de seguir si tiene que ingresar al ensamblado. Alternar entre -O0 y -Os también le permite ver lo que el compilador le está haciendo a su código. Es una educación bastante interesante a veces, y también puede descubrir errores del compilador... ¡esas cosas desagradables que te hacen tirarte de los pelos tratando de descubrir qué está mal con tu código!

Si realmente lo necesito, comenzaré a agregar -fdata-sections y -fcode-sections con --gc-sections, que permiten que el enlazador elimine funciones completas y segmentos de datos que en realidad no se usan. Hay muchas cosas pequeñas con las que puedes jugar para intentar reducir las cosas aún más o hacerlas más rápidas, pero en general estos son los únicos trucos que termino usando, y cualquier cosa que tenga que ser más pequeña o más rápida la daré. -armar.

Sí, deshabilitar las optimizaciones durante la depuración ha sido la mejor práctica desde hace un tiempo, por tres razones:

  • (a) si va a realizar un solo paso del programa con un depurador de alto nivel, es un poco menos confuso.
  • (a) (obsoleto) si va a depurar el programa en un solo paso con un depurador de lenguaje ensamblador, es mucho menos confuso. (Pero, ¿por qué se molestaría con esto cuando podría estar usando un depurador de alto nivel?)
  • (b) (obsoleto durante mucho tiempo) probablemente solo ejecute este ejecutable en particular una vez, luego haga algunos cambios y vuelva a compilar. Es una pérdida de tiempo para una persona esperar 10 minutos adicionales mientras el compilador "optimiza" este ejecutable en particular, cuando eso ahorrará menos de 10 minutos de tiempo de ejecución. (Esto ya no es relevante con las PC modernas que pueden compilar un ejecutable de microcontrolador típico, con optimización completa, en menos de 2 segundos).

Muchas personas van aún más lejos en esta dirección y envían con afirmaciones activadas .

El paso único a través del código ensamblador puede ser muy útil para diagnosticar casos en los que el código fuente especifica algo diferente a lo que parece (por ejemplo, "longvar1 &= ~0x40000000; longvar2 &= ~0x80000000;") o donde un compilador genera código con errores . He rastreado algunos problemas usando depuradores de código de máquina que realmente no creo que hubiera podido rastrear de otra manera.

Simple: las optimizaciones consumen mucho tiempo y pueden ser inútiles si tiene que cambiar ese fragmento de código más adelante en el desarrollo. Así que bien pueden ser una pérdida de tiempo y dinero.
Sin embargo, son útiles para módulos terminados; partes del código que probablemente ya no necesitarán cambios.

Si está utilizando el depurador, deshabilitaría las optimizaciones y habilitaría la depuración.

Personalmente, encuentro que el depurador de PIC causa más problemas de los que me ayuda a solucionar.
Solo uso printf() para USART para depurar mis programas escritos en C18.

La mayoría de los argumentos en contra de habilitar la optimización en su compilación se reducen a:

  1. problemas con la depuración (conectividad JTAG, puntos de interrupción, etc.)
  2. tiempo de software incorrecto
  3. sh*t deja de funcionar correctamente

En mi humilde opinión, los dos primeros son legítimos, el tercero no tanto. A menudo significa que tiene un código incorrecto o confía en una explotación insegura del lenguaje/implementación o tal vez el autor es solo un fanático del buen comportamiento indefinido del tío.

El blog Embedded in Academia tiene un par de cosas que decir sobre el comportamiento indefinido, y esta publicación trata sobre cómo los compiladores lo explotan: http://blog.regehr.org/archives/761

Otra posible razón es que el compilador puede ser molestamente lento cuando se activan las optimizaciones.