Repository navigation
Optimizaciones
- 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:
.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
.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
.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:
- http://gcc.gnu.org/onlinedocs/gcc-4.3.3/gcc/Optimize-Options.html#Optimize-Options
- http://gcc.gnu.org/onlinedocs/gcc/Inline.html
- 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:
- http://avr.2057.n7.nabble.com/char-to-int-promotion-in-bitwise-operators-td10094.html
- http://gcc.gnu.org/bugzilla/show_bug.cgi?id=34791
- 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:
- http://www.atmel.com/Images/doc0936.pdf
- http://bleaklow.com/2012/06/20/sensor_smoothing_and_optimised_maths_on_the_arduino.html
- 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: