Skip to content
Lucas Chiesa edited this page Jul 18, 2015 · 5 revisions

Optimizaciones

Funciones inline

  • Compilador/versión: gcc version 4.3.5 (GCC)
  • Parámetros de compilación: --
  • Condiciones: --
  • Diagnóstico:

En este caso quise hacer una función a la que le pueda pasar un puerto y un número de pin y encienda ese bit. Después probé compilarla con distintos niveles de optimización y esto fue lo que obtuve: Código original en C:

#include <avr/io.h>

void set(volatile uint8_t *port, uint8_t pin)
{
    *port |= 1<<pin;
}

void main(void)
{
    set(&PORTC, PIN7);  // llamado a la función
}

Listado del assembler de salida:

Sin optimización:

.file   "prueba.c" 
__SREG__ = 0x3f
__SP_H__ = 0x3e
__SP_L__ = 0x3d
__CCP__  = 0x34
__tmp_reg__ = 0
__zero_reg__ = 1
    .text
.global set
    .type   set, @function
set:
    push r29
    push r28
    rcall .
    push __tmp_reg__
    in r28,__SP_L__
    in r29,__SP_H__
/* prologue: function */
/* frame size = 3 */
    std Y+2,r25
    std Y+1,r24
    std Y+3,r22
    ldd r30,Y+1
    ldd r31,Y+2
    ld r24,Z
    mov r20,r24
    ldd r24,Y+3
    mov r18,r24
    ldi r19,lo8(0)
    ldi r24,lo8(1)
    ldi r25,hi8(1)
    mov r0,r18
    rjmp 2f
1:  lsl r24
    rol r25
2:  dec r0
    brpl 1b
    or r24,r20
    ldd r30,Y+1
    ldd r31,Y+2
    st Z,r24
/* epilogue start */
    pop __tmp_reg__
    pop __tmp_reg__
    pop __tmp_reg__
    pop r28
    pop r29
    ret
    .size   set, .-set
.global main
    .type   main, @function
main:
    push r29
    push r28
    in r28,__SP_L__
    in r29,__SP_H__
/* prologue: function */
/* frame size = 0 */
    ldi r24,lo8(53)
    ldi r25,hi8(53)
    ldi r22,lo8(7)
    rcall set    // <<<<<<<<<<<<<< llamado a la función
/* epilogue start */
    pop r28
    pop r29
    ret
    .size   main, .-main

Optimización -O1

.file   "prueba.c" 
__SREG__ = 0x3f
__SP_H__ = 0x3e
__SP_L__ = 0x3d
__CCP__  = 0x34
__tmp_reg__ = 0
__zero_reg__ = 1
    .text
.global set
    .type   set, @function
set:
/* prologue: function */
/* frame size = 0 */
    movw r30,r24
    ld r18,Z
    ldi r24,lo8(1)
    ldi r25,hi8(1)
    rjmp 2f
1:  lsl r24
    rol r25
2:  dec r22
    brpl 1b
    or r18,r24
    st Z,r18
/* epilogue start */
    ret
    .size   set, .-set
.global main
    .type   main, @function
main:
/* prologue: function */
/* frame size = 0 */
    ldi r24,lo8(53)
    ldi r25,hi8(53)
    ldi r22,lo8(7)
    rcall set    // <<<<<<<<<<<<<< llamado a la función
/* epilogue start */
    ret
    .size   main, .-main

Optimización -O2

.file   "prueba.c" 
__SREG__ = 0x3f
__SP_H__ = 0x3e
__SP_L__ = 0x3d
__CCP__  = 0x34
__tmp_reg__ = 0
__zero_reg__ = 1
    .text
.global set
    .type   set, @function
set:
/* prologue: function */
/* frame size = 0 */
    movw r30,r24
    ld r18,Z
    ldi r24,lo8(1)
    ldi r25,hi8(1)
    rjmp 2f
1:  lsl r24
    rol r25
2:  dec r22
    brpl 1b
    or r18,r24
    st Z,r18
/* epilogue start */
    ret
    .size   set, .-set
.global main
    .type   main, @function
main:
/* prologue: function */
/* frame size = 0 */
    sbi 53-32,7   // <<<<<<<<<<<<<< el llamado a la función fue reemplazado
/* epilogue start */
    ret
    .size   main, .-main

Al usar la optimización de nivel 2 el compilador no solo reemplaza la llamada a la función por código en línea, sino que además se da cuenta de lo que intenta hacer ese código y lo reemplaza por la instrucción "sbi Rd, K" que en un solo ciclo de máquina enciende el bit K del registro Rd. Bastante óptimo y me alienta a seguir implementado funciones de este tipo.

Artículos relacionados:

Operaciones de bits

  • Compilador/versión: gcc version 4.3.5 (GCC)
  • Parámetros de compilación:
avr-gcc -DMMCU=1 -DF_CPU=18432000UL -mmcu=atmega88 -O2 -g -Wall
  • Condiciones:
pin->pin es uint8_t
pin->bit es uint8_t
  • Diagnóstico: En este ejemplo pin->pin es un port (address 9) y pin->bit = 2.
uint8_t readPin(const IOPIN_t *pin)
{
    return (*(pin->pin) >> pin->bit) & 0x01;
 140:    89 b1           in     r24, 0x09 ; 9  carga el valor del port 9 en el registro r24
 142:    90 e0           ldi    r25, 0x00 ; 0  extiende a 16 bits
 144:    95 95           asr    r25       ; hace 2 rotaciones de bits de 16 bits!
 146:    87 95           ror    r24
 148:    95 95           asr    r25
 14a:    87 95           ror    r24

A pesar de que todas la variables involucradas son de 8 bits, extiende el operando a 16 bits consumiendo más ciclos de clock. Es subóptimo.

El mismo código ahora compilado con la opción -mint8 que fuerza el tipo int a 8 bits:

avr-gcc -DMMCU=1 -DF_CPU=18432000UL -mint8 -mmcu=atmega88 -O2 -g -Wall
uint8_t readPin(const IOPIN_t *pin)
{
    return (*(pin->pin) >> pin->bit) & 0x01;
 138:    89 b1           in    r24, 0x09    ; 9
 13a:    86 95           lsr    r24
 13c:    86 95           lsr    r24

Ahora el código sí resulta óptimo. Sin embargo hay que tener en cuenta las posibles consecuencias de reducir todos los ints (ya que los longs también se reducen a 16 bits, etc)

Artículos relacionados:

División y aritmética modular

  • Compilador/versión: --
  • Parámetros de compilación: --
  • Condiciones: --
  • Diagnóstico: Para obtener el módulo, también conocido como resto de la división entera, es necesario hacer una división. Fuimos a la hoja de datos para conocer cuánto tardaba la instrucción de división y para nuestra sorpresa nos encontramos con que la arquitectura AVR de 8 bits no posee una instrucción de división. Al observar esto, lo primero que hicimos fue ver qué código genera el compilador cuando usamos una división. Por ejemplo, para el siguiente código:
#include <avr/io.h>

void main(void)
{    
    DDRC = DDRC % 5;

    PORTC = PORTC / 7;
}

El compilador genera el siguiente código (optimización O2):

00000038 <main>:
  38:    84 b3       in    r24, 0x14    ; 20
  3a:    65 e0       ldi    r22, 0x05    ; 5
  3c:    06 d0       rcall    .+12     ; 0x4a <__udivmodqi4>
  3e:    94 bb       out    0x14, r25    ; 20
  40:    85 b3       in    r24, 0x15    ; 21
  42:    67 e0       ldi    r22, 0x07    ; 7
  44:    02 d0       rcall    .+4          ; 0x4a <__udivmodqi4>
  46:    85 bb       out    0x15, r24    ; 21
  48:    08 95       ret

0000004a <__udivmodqi4>:
  4a:    99 1b       sub    r25, r25
  4c:    79 e0       ldi    r23, 0x09    ; 9
  4e:    04 c0       rjmp    .+8          ; 0x58 <__udivmodqi4_ep>

00000050 <__udivmodqi4_loop>:
  50:    99 1f       adc    r25, r25
  52:    96 17       cp    r25, r22
  54:    08 f0       brcs    .+2          ; 0x58 <__udivmodqi4_ep>
  56:    96 1b       sub    r25, r22

00000058 <__udivmodqi4_ep>:
  58:    88 1f       adc    r24, r24
  5a:    7a 95       dec    r23
  5c:    c9 f7       brne    .-14     ; 0x50 <__udivmodqi4_loop>
  5e:    80 95       com    r24
  60:    08 95       ret

La función __udivmodqi4 espera en el registro r24 el dividendo y en el r22 el divisor. A su término, almacena el cociente en el registro r24 y el resto en el r25. Pareciera que el algoritmo que usa es similar a este: http://en.wikipedia.org/wiki/Division_algorithm#Restoring_division

En definitiva para hacer la división de valores de 8 bits usa muchas instrucciones y un bucle de 8 ciclos (uno por bit). Podemos comparar con el INTEL 8051 que sí tiene una instrucción de división que demora 4 ciclos de máquina. En esa arquitectura cada ciclo de máquina emplea 12 ciclos de reloj. Si trabajamos a 12 MHz, la instrucción de división tardará 4us en ejecutarse. El AVR por su parte tiene una arquitectura RISC y ejecuta una instrucción por cada ciclo de reloj. Para hacer la división tiene que ejecutar unas 62 instrucciones (peor caso), por lo que tardará 62*1/12us = 5us. La demora está en el mismo orden que el 8051 pero un 25% mayor.

Conclusión: el Atmega88 (y todos sus amigos, excepto los de 32 bits) no tiene instrucción de división. Cuando hacemos divisiones o usamos el operador módulo pierde bastante rendimiento y se vuelve comparable a otras arquitecturas. La próxima vez voy a pensar dos veces antes de usar una división o el operador módulo.

Artículos relacionados:

Excluir funciones no usadas en link estático

  • Compilador/versión: gcc
  • Parámetros de compilación:
-O2 -g -Wall -fdata-sections -ffunction-sections -Wl,-gc-sections,-print-gc-sections
  • Condiciones: --
  • Diagnóstico: Para no ocupar mucho espacio con código innecesario deseamos incluir en el linkeo final sólo aquellas funciones que son usadas por nuestro programa. Esto se puede lograr armando una biblioteca de código pero sólo si cada función se compila en un archivo objeto separado. La otra forma es dejar que el linker identifique y elimine el código muerto usando las siguientes opciones:
-fdata-sections -ffunction-sections -Wl,-gc-sections,-print-gc-sections

Esto le indica al compilador que coloque cada función en una sección distinta y luego le indicamos al linker que haga un "garbage collect" y limpie las secciones. Hay que tener cuidado con funciones de interrupción ya que el linker no identificará que alguien la llama explícitamente y puede llegar a eliminarla.

Artículos relacionados: