AVR GCC: ¿Cómo mejoro la optimización del código?

Traté de compilar el siguiente código C:

period = TCNT0L;
period |= ((unsigned int)TCNT0H<<8);

El código ensamblador que obtengo es el siguiente:

    period = TCNT0L;
  d2:   22 b7           in  r18, 0x32   ; 50
  d4:   30 e0           ldi r19, 0x00   ; 0
  d6:   30 93 87 00     sts 0x0087, r19
  da:   20 93 86 00     sts 0x0086, r18
    period |= ((unsigned int)TCNT0H<<8);
  de:   44 b3           in  r20, 0x14   ; 20
  e0:   94 2f           mov r25, r20
  e2:   80 e0           ldi r24, 0x00   ; 0
  e4:   82 2b           or  r24, r18
  e6:   93 2b           or  r25, r19
  e8:   90 93 87 00     sts 0x0087, r25
  ec:   80 93 86 00     sts 0x0086, r24

Entonces, en lugar de 4 instrucciones, ¡tiene 11!

Traté de elegir las opciones de optimización O1, O2, O3 y Os. El resultado es el mismo (excepto que la O3opción optimizó este código).

Podría escribir el código fuente de la siguiente manera:

period = TCNT0L | ((unsigned int)TCNT0H<<8);

Obtendré un código más pequeño, pero aún no óptimo:

  de:   22 b7           in  r18, 0x32   ; 50
  e0:   34 b3           in  r19, 0x14   ; 20
  e2:   93 2f           mov r25, r19
  e4:   80 e0           ldi r24, 0x00   ; 0
  e6:   82 2b           or  r24, r18
  e8:   90 93 87 00     sts 0x0087, r25
  ec:   80 93 86 00     sts 0x0086, r24

Sin embargo, ya no tendré garantía de que se accederá primero al byte inferior (este es un requisito esencial para mantener la lectura correcta de 16 bits). Y aún así el código tiene muchas instrucciones adicionales innecesarias.

¿Puedo cambiar las opciones del compilador y/o cambiar el código fuente para mejorarlo? Evitaría ir al ensamblador.

ACTUALIZACIÓN1:

Probé el código que sugirió @caveman:

((unsigned char*)(&period))[0] = TCNT0L;
((unsigned char*)(&period))[1] = TCNT0H;

Pero el resultado tampoco es muy bueno:

    ((unsigned char*)(&period))[0] = TCNT0L;
  dc:   82 b7           in  r24, 0x32   ; 50
  de:   e6 e8           ldi r30, 0x86   ; 134
  e0:   f0 e0           ldi r31, 0x00   ; 0
  e2:   80 83           st  Z, r24
    ((unsigned char*)(&period))[1] = TCNT0H;
  e4:   84 b3           in  r24, 0x14   ; 20
  e6:   81 83           std Z+1, r24    ; 0x01
¿No puedes simplemente hacer: uint8_t period = TCNT0 ?
@Golaž esto es extraño, pero mi compilador no acepta TCNT0 del estante. ¿Necesito declararlo yo mismo? En caso afirmativo, ¿cómo declaro registros de 16 bits (teniendo en cuenta el hecho de que el procesador es de 8 bits)?
¿Qué IDE estás usando?
@Golaž Atmel Estudio 6
Una cosa es que si le preocupa que el LSB se lea primero, es posible que esté haciendo las cosas mal. Digamos, por ejemplo, en el 0x00FFpunto 0x0100en el que podría terminar leyendo 0x01FF.
@PeterJ 8bit AVR proporciona bloqueo de mordida alto después de la mitad inferior de la lectura de registro de 16 bits
@Roman ahh no estaba al tanto de eso.
"Escribir un código diferente" no parece ser una respuesta a "cómo puedo hacer que mi compilador se optimice mejor".

Respuestas (4)

Un método es usar cargas directas a las mitades del período. Si bien esto parece complicado en C, generalmente generará un ensamblaje muy ajustado, es decir, 2 cargas y 2 tiendas.

((uint8_t*)(&period))[0] = TCNT0L;
((uint8_t*)(&period))[1] = TCNT0H;

A veces, usar la matriz matemática puede causar problemas, por lo que puede intentar esto:

*((uint8_t*)(&period)) = TCNT0L;
*((uint8_t*)(&period) + 1) = TCNT0H;

Esto realmente produce un código óptimo. Mira cómo se usan 12 bytes.

  ((unsigned char*)(&period))[0] = TCNT0L;
  dc:   82 b7           in  r24, 0x32   ; 50
  de:   e6 e8           ldi r30, 0x86   ; 134
  e0:   f0 e0           ldi r31, 0x00   ; 0
  e2:   80 83           st  Z, r24
    ((unsigned char*)(&period))[1] = TCNT0H;
  e4:   84 b3           in  r24, 0x14   ; 20
  e6:   81 83           std Z+1, r24    ; 0x01

Si hizo esto con ensamblaje, probablemente parecería mejor hacerlo así. También es de 12 bytes, por lo que son equivalentes.

  dc:   82 b7           in  r24, 0x32   ; 50
  de:   80 93 86 00     sts 0x0086, r24
  e2:   84 b3           in  r24, 0x14   ; 20
  e4:   80 93 87 00     sts 0x0087, r24

Por supuesto, cuando digo "equivalente", me refiero al tamaño del código. Si el tiempo es más importante, entonces hay que mirar los ciclos. En este caso parece que la versión del ensamblador es de 6 ciclos y la versión del compilador es de 8 ciclos.

Probé tu código. Trabajó parcialmente. 7 instrucciones que equivalen al enfoque de un operador :( (ver mi pregunta UPDATE1)
Puede intentar usar esta versión matemática sin matrices. Pero, francamente, solo use el ensamblaje en línea. El generador de código parece ser bastante malo en este puerto de gcc.
Intenté el segundo: mismo resultado. Con respecto a R30/R31, parece que intenta usar el almacenamiento indirecto (ya que he usado el método de direccionamiento indirecto en el código), por lo que parece bastante garantizado.
Acabo de notar su última edición: mi principal preocupación es el tiempo de ejecución. Así que la versión de ensamblador es lo que realmente necesito. Entonces, ¿puedo suponer que este código no se puede obtener correctamente del compilador C (sin usar el ensamblador en línea)?
No creo que el compilador pueda hacerlo mucho mejor. Has probado muchas cosas. Pero déjame preguntarte si has retrocedido y realmente crees que 2 ciclos valen la pena. Si todo su código es tan ajustado, probablemente debería usar ensamblador directamente de todos modos.
Parece que debería ir a ensamblador. Tengo un ISR que debería reaccionar lo más rápido posible. Solo tengo 1,5 us en mi "presupuesto" y muchas cosas que hacer (además de la parte del código que describí en mi pregunta original).
Buena respuesta, pero le recomiendo que agregue una nota de que este tipo de conversión de puntero no es portátil y, en general, es una mala práctica (como mínimo, debe estar protegido por una aserción). Habiendo dicho eso, este tipo de cosas es bastante común en el mundo integrado.

En mi avr-gcc 5.4.0 simple period = TCNT1;para attiny841 parece emitir el código así:

    in  r24,0x2c
    in  r25,0x2d
    sts 0x0110,r25
    sts 0x010f,r24

Parece que el compilador ya conoce la forma en que se debe acceder a los registros de 16 bits y, por lo tanto, el código como el anterior es seguro.

La rama avr de gcc generalmente no es muy buena, incluso en optimizaciones simples como los ejemplos en la pregunta, pero de todos modos, actualizar la versión de avr-gcc a menudo ayuda.

Otra preocupación es que gccs posteriores y avr-libcs ​​posteriores podrían admitir el acceso a TCNT0 como registro único de 16 bits, lo que parece faltar en el gcc utilizado en la pregunta.

¡Ahhh! ¡Odio cada vez que veo este motivo emitido! ¿Por qué el compilador siempre desperdicia un registro adicional en lugar de hacer el equivalente?
@bigjosh Especialmente doloroso cuando todo lo que quieres es incrementar un uint32_t(o más) en un ISR. En estos casos, el compilador a veces puede salirse con la suya con 0 registros (¡use el registro temporal!) pero empuja cuatro de ellos a la pila y los vuelve a abrir al regresar. Tengo una macro asm lista para esos casos especiales.

Si está dispuesto a desperdiciar un pin, puede obtener una captura de TCNT de 1 instrucción/2 ciclos cuando se llama al ISR utilizando la Unidad de captura de salida.

Configuración

  1. Establezca un bit en el pin DDR para ICP para convertirlo en una salida.
  2. Configure ACIC para usar el pin de entrada para el disparador ICU. Deje otros bits de ICU a los valores predeterminados (sin filtro de ruido, disparador en el borde descendente)

Por cada captura

en primer plano

  1. Borre el bit ICF escribiendo un 1 en él.
  2. Establezca el bit de PUERTO para el pin ICP para que salga ALTO.
  3. Sondee el bit ICF hasta que cambie a 1.
  4. Lea TNCTel valor capturado fuera del ICRregistro.
  5. Enjuagar. Repetir.

en ISR

  1. Establezca el bit PORT para el pin ICP usando la instrucción SBI.

ingrese la descripción de la imagen aquí

ingrese la descripción de la imagen aquí

ingrese la descripción de la imagen aquí

Si desea ir a fondo con el ahorro de ciclos, ¡podría reducir esta captura de TCNT a un solo ciclo en el ISR!

Puede aprovechar el hecho de que el byte alto del registro TCNT se almacena en el búfer cada vez que se lee el byte bajo.

ingrese la descripción de la imagen aquí

Entonces, si preasignó un registro (por ejemplo, r16) para esta tarea...

register unsigned char tcnt_low_byte asm("r16");

...luego llenó este registro con el byte bajo del TCNTinterior del ISR así...

R16 = TCNTL;

...que debería compilarse hasta el ciclo 1...

IN R16,TCNTL

... luego podría leer el TCNTvalor completo de la instantánea en primer plano de esta manera ...

period = (TCNTH << 8)| R16;

Solo asegúrese de leer TCNTHantes de acceder a cualquier otro registro de temporizador de 16 bits, ya que todos ellos comparten ese registro temporal.

El trabajo total realizado en el ISR es solo uno, in R16, TCNTLque es 1 ciclo.

El OP no indicó cómo señalaría el proceso de primer plano que ocurrió un ISR, pero si estaba precargando periody 0luego buscando un cambio, entonces se necesita algo de trabajo adicional...

  1. precargar 0en el TEMPregistro de 16 bits (puede hacerlo escribiendo a 0en cualquier registro de 16 bits).
  2. precargar 0en R16.

Luego puede sondear para ver si el ISR sucedió con...

x=TCNTH
if (x || R16) {
    period=(x<<8 | R16)
    // Process new period capture here...
}
Idea interesante. Pero, ¿cómo comunicaría desde el ISR al bucle principal que se ha leído TCNTL y que es hora de leer TCNTH?
@JimmyB Gran pregunta, ¡no había considerado eso! ¿Presumiblemente usando cualquier mecanismo que el OP usó para señalar que el valor TCNT había sido capturado en periodsu ejemplo (no se muestra)? Respuesta actualizada para el caso si su mecanismo fuera preestablecer el período 0y luego sondearlo.
@JimmyB ¿Ves algún problema? Creo que el acceso a x+R16 ni siquiera necesita ser atómico.