Por favor, no desvirtuemos el espíritu del hilo (ver #1).
Todos estamos de acuerdo en la opinión de cosmefulanito04. Incluso yo mismo se lo he aconsejado a otro miembro, en otro hilo, cuando le he dicho que por unos céntimos más tiene el doble de memoria, así que no perdiera más el tiempo.
En cambio, este hilo es justo lo contrario: partimos del interés en intentar hacer el código más eficiente, aunque sea ahorrando medio byte (un nibble).
Entre los tips de C los tips de optimización son solo una parte. No todos los tips tienen por qué ser de optimización.
Me pareció que era una crítica válida no invertir un esfuerzo excesivo en optimizar sacrificando por ello cosas como capacidad de expansión, claridad de código, etc.
Pero bueno, ya se dijo de sobra, siendo que tiramos una de cal, va otra de arena: dividir por una constante cuando en el micro hay un multiplicador por hardware.
Los micros de medio pelo para arriba incorporan multiplicadores hardware, mínimo de 8 bits (con resultado de 16 bits), y se suele dar el caso donde es necesario dividir por una constante conocida de antemano.
Que pasa si queremos hacer algo como:
uint8_t variable = otraVariable/17; //otraVariable también es de 8 bits
Si tengo un multiplicador de 8 bits que me da un resultado en 16 bits (digamos que da el resultado en 2 registros tamaño byte llamados RESL y RESH, siendo parte baja y alta respectivamente), puedo hacer la división con una multiplicación:
otraVariable / 17 = otraVariable /17 * (256 / 256) = otraVariable * (256/17) / 256
La división por 256 está implícita al quedarnos con la parte alta del resultado de la multiplicación (con el registro que llamamos RESH).
256/17 = 15.0588... no es un número redondo, voy a tener que convivir con el error de redondearlo a 15.
Entonces, en vez de escribir la línea
Código:
uint8_t variable = otraVariable/17;
Puedo hacer
Código:
uint8_t variable;
uint16_t variable16 = otraVariable * 15;
variable = ParteAlta(variable16);
Donde parteAlta puede ser una macro que devuelva el byte alto de una variable de 16 bits.
Ejemplo con números:
Si variable = 210
Queremos obtener 210/17 = 12.35 -> 12
Para hacer la división multiplicando el factor será 256/17 = 15.05.. -> 15
La cuenta que vamos a hacer es 210*15 = 3150 = 0x0C4E
Parte alta = 0x0C = 12 (por lo menos dió sin error en este caso particular, aunque no es lo normal, suele haber error).
¿Y sirve de algo meterse en este menester?. Puede que sí, puede que no. A veces pasa que el compilador ignora el multiplicador hardware, o al menos no lo usa para hacer divisiones por constantes, aunque sí para multiplicaciones.
Para un micro ARM de 32 bits no creo que sirva porque ya tienen unidades aritmético lógicos muchos más potentes (división incluída) y código bastante maduro. Puede andar para micros de 8 bits (si tienen multiplicador y una ALU sin división).
¿Y tanto le cuesta a un micro de 8 bits hacer una división?, y... gratis no le sale.
Según recuerdo para compilador gcc (o derivados, cada vez es más común que los compiladores para micros sean derivados de gcc) eso viene en la librería stdlib/libgcc donde hay funciones con nombres *divmod*.
A ver si lo encuentro... uff, costó... acá está la versión para AVR de la página oficial de gcc (tarda un rato largo en cargar)
https://gcc.gnu.org/viewcvs/gcc/trunk/libgcc/config/avr/lib1funcs.S
La función de división 8/8 está hecha en assembler:
https://gcc.gnu.org/viewcvs/gcc/trunk/libgcc/config/avr/lib1funcs.S?view=markup#l1335
Código (no borré los números de línea):
Código:
1335 .section .text.libgcc.div, "ax", @progbits
1336
1337 /*******************************************************
1338 Division 8 / 8 => (result + remainder)
1339 *******************************************************/
1340 #define r_rem r25 /* remainder */
1341 #define r_arg1 r24 /* dividend, quotient */
1342 #define r_arg2 r22 /* divisor */
1343 #define r_cnt r23 /* loop count */
1344
1345 #if defined (L_udivmodqi4)
1346 DEFUN __udivmodqi4
1347 sub r_rem,r_rem ; clear remainder and carry
1348 ldi r_cnt,9 ; init loop counter
1349 rjmp __udivmodqi4_ep ; jump to entry point
1350 __udivmodqi4_loop:
1351 rol r_rem ; shift dividend into remainder
1352 cp r_rem,r_arg2 ; compare remainder & divisor
1353 brcs __udivmodqi4_ep ; remainder <= divisor
1354 sub r_rem,r_arg2 ; restore remainder
1355 __udivmodqi4_ep:
1356 rol r_arg1 ; shift dividend (with CARRY)
1357 dec r_cnt ; decrement loop counter
1358 brne __udivmodqi4_loop
1359 com r_arg1 ; complement result
1360 ; because C flag was complemented in loop
1361 ret
1362 ENDF __udivmodqi4
1363 #endif /* defined (L_udivmodqi4) */
Como se ve usa un algoritmo con un loop que se repite 8 veces donde se hacen comparaciones, saltos, decremento, y resta. Pongamos un número redondo (a mi favor
) de 50 instrucciones.
Si eso se puede reemplazar por 2 instrucciones entonces no está tan mal (ok ok, no son solo 2, digamos que pueden rondar las 5?).
No tengo un compilador instalado para AVR en este momento, ¿alguien lo puede compilar a ver que pasa?.
Código 1 (normal):
Código:
#include blabla... lo que requiere el avr
#include <stdint.h>
uint8_t dividendo;
uint8_t divisor;
void main(void)
{
//TODO Lo que requiera el micro para inicializar
divisor = dividendo/17;
while(1)
{
nop();
}
}
Código 2 (optimizado?):
Código:
#include blabla... lo que requiere el avr
#include <stdint.h>
uint8_t dividendo;
uint8_t divisor;
uint16_t mul;
void main(void)
{
//TODO Lo que requiera el micro para inicializar
mul = dividendo * 15;
divisor = high(mul);
while(1)
{
nop();
}
}
Ojalá no haya escrito de gusto
Actualización
Ojalá no haya escrito de gusto
-> SI, escribí de gusto... ahí va el descargo.
Me instalé el avr gcc (para Ubuntu) siguiendo:
http://maxembedded.com/2015/06/setting-up-avr-gcc-toolchain-on-linux-and-mac-os-x/
Afortunadamente, parece que siguiendo las instrucciones al pie de la letra no hay ningún error raro, pude compilar el código de ejemplo sin ningún problema (y también lo hice fallar borrando un punto y coma y tiro el error como debe ser).
Listo, vamos a compilar para atmega32u4 que es popular y tiene multiplicador por hardware.
Archivo divNormal.c:
Código:
//Compilar con:
//avr-gcc -g -O2 -mmcu=atmega32u4 -c divNormal.c
//avr-gcc -g -mmcu=atmega32u4 -o divNormal.elf divNormal.o
//Desensamblar con:
//avr-objdump -S divNormal.elf > divNormalDisAsm.txt
//Compilar sin optimizar
//avr-gcc -g -O0 -mmcu=atmega32u4 -c divNormal.c
//avr-gcc -g -mmcu=atmega32u4 -o divNormalO0.elf divNormal.o
//avr-objdump -S divNormalO0.elf > divNormalDisAsmO0.txt
#ifndef F_CPU
#define F_CPU 16000000UL // or whatever may be your frequency
#endif
#include <avr/io.h>
#include <stdint.h>
uint8_t dividendo = 148;
uint8_t divisor;
int main(void)
{
//TODO Lo que requiera el micro para inicializar
divisor = dividendo/17;
while(1)
{
;
}
return 0;
}
Archivo divOpt.c:
Código:
//Compilar con:
//avr-gcc -g -O2 -mmcu=atmega32u4 -c divOpt.c
//avr-gcc -g -mmcu=atmega32u4 -o divOpt.elf divOpt.o
//Desensamblar con:
//avr-objdump -S divOpt.elf > divOptDisAsm.txt
#ifndef F_CPU
#define F_CPU 16000000UL // or whatever may be your frequency
#endif
#include <stdint.h>
uint8_t dividendo = 148;
uint8_t divisor;
uint16_t mul;
void main(void)
{
//TODO Lo que requiera el micro para inicializar
mul = dividendo * 15;
divisor = high(mul);
while(1)
{
nop();
}
}
Compilar y desensamblar como dicen los comentarios, queda
Archivo divNormalDisAsm.txt (solo pongo la parte relevante)
Código:
int main(void)
{
//TODO Lo que requiera el micro para inicializar
divisor = dividendo/17;
ea: 90 91 00 01 lds r25, 0x0100
ee: 81 ef ldi r24, 0xF1 ; 241
f0: 98 9f mul r25, r24
f2: 81 2d mov r24, r1
f4: 11 24 eor r1, r1
f6: 82 95 swap r24
f8: 8f 70 andi r24, 0x0F ; 15
fa: 80 93 02 01 sts 0x0102, r24
fe: ff cf rjmp .-2 ; 0xfe <main+0x14>
00000100 <_exit>:
100: f8 94 cli
00000102 <__stop_program>:
102: ff cf rjmp .-2 ; 0x102 <__stop_program>
Archivo divOptDisAsm.txt:
Código:
000000d4 <main>:
uint8_t dividendo = 148;
uint8_t divisor;
uint16_t mul;
void main(void)
{
d4: 90 91 01 01 lds r25, 0x0101
d8: 81 ef ldi r24, 0xF1 ; 241
da: 98 9f mul r25, r24
dc: 81 2d mov r24, r1
de: 11 24 eor r1, r1
e0: 82 95 swap r24
e2: 8f 70 andi r24, 0x0F ; 15
e4: 80 93 00 01 sts 0x0100, r24
e8: ff cf rjmp .-2 ; 0xe8 <main+0x14>
000000ea <_exit>:
ea: f8 94 cli
000000ec <__stop_program>:
ec: ff cf rjmp .-2 ; 0xec <__stop_program>
Como se ve el compilador generó código (casi) idéntico.. así que nada, por lo menos me queda el compilador para avr instalado
.
Ni siquiera sirve cuando se compila sin optimización (con O0), así que a llorar a la iglesia