¿Por qué el compilador GCC omite algún código?

No puedo entender por qué el compilador GCC elimina parte de mi código mientras conserva absolutamente el mismo en el vecindario.

El código C:

#define setb_SYNCO do{(PORTA|= (1<<0));} while(0);

ISR(INT0_vect){
    unsigned char i;

    i = 10;
    while(i>0)i--;   // first pause - omitted

    setb_SYNCO;
    setb_GATE;
    i=30;
    clrb_SYNCO;
    while(i>0)i--;  // second pause - preserved
    clrb_GATE;
}

La parte correspondiente de LSS (archivo ensamblador, creado por el compilador):

ISR(INT0_vect){
  a4:   1f 92           push    r1
  a6:   0f 92           push    r0
  a8:   0f b6           in  r0, 0x3f    ; 63
  aa:   0f 92           push    r0
  ac:   11 24           eor r1, r1
  ae:   8f 93           push    r24
    unsigned char i;

    i = 10;
    while(i>0)i--;

    setb_SYNCO;
  b0:   d8 9a           sbi 0x1b, 0 ; 27
    setb_GATE;
  b2:   d9 9a           sbi 0x1b, 1 ; 27
    i=30;
    clrb_SYNCO;
  b4:   d8 98           cbi 0x1b, 0 ; 27
  b6:   8e e1           ldi r24, 0x1E   ; 30
  b8:   81 50           subi    r24, 0x01   ; 1
    while(i>0)i--;
  ba:   f1 f7           brne    .-4         ; 0xb8 <__vector_1+0x14>
    clrb_GATE;
  bc:   d9 98           cbi 0x1b, 1 ; 27
}
  be:   8f 91           pop r24
  c0:   0f 90           pop r0
  c2:   0f be           out 0x3f, r0    ; 63
  c4:   0f 90           pop r0
  c6:   1f 90           pop r1
  c8:   18 95           reti

Podría suponer que el compilador se da cuenta de que dicho código es ficticio y lo corta, pero ¿por qué conserva el mismo al final del código?

¿Hay alguna instrucción del compilador para evitar dicha optimización?

intente declarar i como volátil también, es decir, char i volátil sin firmar;
Gracias, @VladimirCravero. Parcialmente funcionó. Sin embargo, mi código creció significativamente a medida que ise asignó var en RAM
También puede decirle al compilador que no optimice una sola función, tal vez valga la pena probar este método con su ISR. Vea esta pregunta en stackoverflow.
Hola Roman, agregué la etiqueta "c" a tu pregunta eliminando atmega. Tuve que quitar una etiqueta ya que hay un límite (cinco), y al hacer una pregunta relacionada con el código, agregar el nombre del idioma como etiqueta es excelente porque todo el código (preguntas y respuestas) se resalta.
@VladimirCravero ¡Está bien! Gracias por su aporte
En términos generales, los lenguajes de nivel superior (como C) están diseñados explícitamente para no estar vinculados a una relación 1:1 con su ensamblaje resultante. Si necesita contar las instrucciones para obtener el tiempo correcto, siempre debe confiar en el ensamblaje (como lo hicieron algunas de las respuestas). El objetivo de los lenguajes de alto nivel es que el compilador tiene cierta libertad para ayudar a que su código sea más rápido, más fuerte y mejor que antes. Es mucho mejor dejar detalles como las asignaciones de registros y las predicciones de bifurcaciones al compilador... excepto en momentos como este en los que usted, el programador, sabe exactamente las instrucciones que desea.
La mejor pregunta es, ¿por qué GCC no está optimizando ambos bucles?
¡@IlmariKaronen definitivamente!
Esperaría que se deshiciera de ambos, o tal vez los reemplazara con una asignación directa a cero. ¿Cuáles son las definiciones de las macros?
@ Random832, agregué una definición de macro a la respuesta inicial. Otros son muy similares.
Gcc primero desenrolla el bucle y solo entonces se da cuenta de que el código correspondiente es inútil. Con un tamaño de bucle de 30, desenrollar sería una tontería y gcc no lo hace. En un nivel de optimización más alto, ambos están optimizados.
Es debido al análisis de flujo. Disminuye ide 10a 0, pero el valor actualizado nunca se usa: después del bucle hay una definición ( i = 30) pero no se usa (me gusta y = i + 1). Entonces, cualquier valor que ituviera antes de la i = 30parte no es relevante. El whilebucle se marca como inútil y no se emite el código correspondiente.
@StefanoSanfilippo pero i es una variable local y muere dos líneas después del segundo ciclo, y nunca se lee. Estoy poniendo mis fichas en la explicación de Marc justo arriba.

Respuestas (4)

Dado que en un comentario afirma que "cada tic de la CPU vale la pena", sugiero usar algún ensamblaje en línea para hacer que sus retrasos se reproduzcan como desee. Esta solución es superior a las varias volatileo -O0porque deja en claro cuál es su intención.

unsigned char i = 10;
__asm__ volatile ( "loop: subi    %0, 0x01\n\t"
                   "      brne    loop"
                   : "+rm" (i)
                   : /* no inputs */
                   : /* no dirty registers to decleare*/);

Eso debería hacer el truco. Lo volátil está ahí para decirle al compilador "Sé que esto no hace nada, solo guárdelo y confíe en mí". Las tres "declaraciones" de asm se explican por sí mismas, puede usar cualquier registro en lugar de r24, creo que al compilador le gustan los registros más bajos, por lo que es posible que desee usar uno alto. Después del primero :, debe enumerar las variables c de salida (leer y escribir), y no hay ninguna, después del segundo :debe enumerar las variables c de entrada (solo), nuevamente, no hay ninguna, y el tercer parámetro es una lista separada por comas de registros modificados , en este caso r24. No estoy seguro de si debe incluir también el registro de estado ya que la ZERObandera cambia, por supuesto, no lo incluí.

edite la respuesta editada como OP solicitó. Algunas notas.

Lo "+rm"anterior (i)significa que está dejando que el compilador decida colocar i en la memoria o en un registro. Eso es bueno en la mayoría de los casos, ya que el compilador puede optimizar mejor si es gratuito. En su caso, creo que desea mantener solo la restricción r para obligar a i a ser un registro.

Parece que esto es algo que realmente necesito. Pero, ¿podría modificar su respuesta para aceptar cualquier cvariable en lugar del literal 10que mencioné en la respuesta original? Estoy tratando de leer los manuales de GCC con respecto al uso adecuado de la construcción de ASM , pero ahora está un poco oscuro para mí. te lo agradeceria mucho!
@RomanMatveev editado como usted solicitó

Podría intentar hacer que el bucle realmente haga algo. Tal como está, el compilador dice con razón "Este bucle no hace nada, me desharé de él".

Así que podrías probar una construcción que uso con frecuencia:

int i;
for (i = 0; i < 10; i++) {
    asm volatile ("nop");
}

Nota: no todos los objetivos para el compilador gcc usan la misma sintaxis de ensamblaje en línea; es posible que deba modificarlo para su objetivo.

Su solución parece mucho más elegante que la mía ... ¿tal vez la mía es mejor cuando se requiere un recuento de ciclos PRECISO? Quiero decir, no se garantiza que el todo sea compilado de cierta manera, ¿o sí?
El simple hecho de que está usando C significa que no puede garantizar los ciclos. Si necesita un conteo de ciclos preciso, ASM es el único camino a seguir. Quiero decir, obtienes una sincronización diferente con el bucle C si tienes -funroll-loops activado que si no lo tienes, etc.
Sí, eso es lo que yo pensaba. Al hacer retrasos HW con valores i lo suficientemente altos (100 o más), supongo que su solución produce prácticamente los mismos resultados al tiempo que mejora la legibilidad.

Sí, podrías suponer eso. Si declara la variable i como volátil, le dice al compilador que no optimice en i.

Eso no es del todo cierto en mi opinión.
Desafortunadamente, esto le dice al compilador no solo sobre la forma de optimización, sino que también lo obliga a asignar la variable ien la RAM para que mi código se vuelva significativamente más grande y más lento. En mi caso, cada marca de la CPU vale la pena, por lo que me gustaría encontrar una forma de controlar la optimización en línea si es posible.
@VladimirCravero, ¿qué quieres decir con eso? ¿Podrías dejar más claro?
Lo que quise decir es que no estaría tan seguro de lo que hace un compilador. Declarar una variable volátil le dice al compilador que podría cambiar en otro lugar, por lo que realmente debería hacerlo mientras.
Esta es una respuesta correcta. Tenga en cuenta que poner iun registro ya es una optimización, por lo tanto, el primer comentario de @Roman es un malentendido.
@Roman Matveev register unsigned char volatile i __asm__("r1");tal vez?
Declarar icomo volátil resuelve todo. Esto está garantizado por el estándar C 5.1.2.3. Un compilador conforme no debe optimizar esos bucles si ies volátil. Afortunadamente, GCC es un compilador conforme. Desafortunadamente, hay muchos posibles compiladores de C que no cumplen con el estándar, pero eso es irrelevante para esta pregunta en particular. No hay absolutamente ninguna necesidad de ensamblador en línea.

Después del primer ciclo ies una constante. La inicialización de iy del ciclo no hace más que producir un valor constante. Nada en el estándar especifica que este bucle debe compilarse tal cual. La norma tampoco dice nada sobre el tiempo. El código compilado debe comportarse como si el bucle estuviera presente y lo hace. No puede decir de manera confiable que esta optimización se realizó bajo el estándar (el tiempo no cuenta).

El segundo bucle también debe eliminarse. Considero que es un error (o una optimización faltante) que no lo es. Después del bucle ies cero constante. El código debe ser reemplazado con el ajuste ia cero.

Creo que GCC se mantiene isimplemente por la razón de que puede afectar el acceso a un puerto (opaco) i.

Utilizar

asm volatile ("nop");

engañar a GCC para que crea que el ciclo hace algo.