A continuación se muestra un código implementado en un microcontrolador de 8 bits.
El siguiente comentario fue publicado en otra pregunta :
Como su código no usa la
i
variable, ¿por qué no solowhile(length--)
en lugar del primerfor
ciclo?
Sugiere cambiar el for(..)
bucle por un while(..)
, ¿supondrá esto una diferencia práctica en la optimización del código para un microcontrolador integrado?
uint32_t MyFunction(uint32_t crc, uint8_t *buffer, uint16_t length) {
for (uint16_t i=0; i < length; i++) {
crc = crc ^ *buffer++;
for (uint16_t j=0; j < 8; j++) {
if (crc & 1)
crc = (crc >> 1) ^ 0xEDB88320;
else
crc = crc >> 1;
}
}
return crc;
}
Nota: Escribí esta pregunta y la respondí yo mismo después de publicar un comentario a principios de esta semana. Ese comentario fue votado a favor, pero sentí que debería respaldarlo con algunas pruebas reales. Serían útiles otras respuestas, pero esto pretendía ser una prueba genérica de un punto en lugar de una pregunta específica sobre el algoritmo.
Las tres reglas clásicas de optimización:
No está de más escribir un código limpio y ordenado en primer lugar, pero la optimización prematura es una pérdida de tiempo y esfuerzo que podría gastarse de manera más fructífera en otro lugar.
Lo primero que hay que tener en cuenta es lo que hay que optimizar. ¿El código necesita ocupar menos memoria de programa o debería ejecutarse más rápido? A veces, estas cosas se pueden lograr reduciendo la cantidad de instrucciones, sin embargo, esto depende del microcontrolador subyacente y la cantidad de ciclos que toma cada instrucción.
Es completamente posible generar código más pequeño que requiera más ciclos para ejecutarse, especialmente si hay sucursales involucradas.
Para comenzar a experimentar, es importante aislar la porción de código más pequeña posible. Creé el caso de prueba simple a continuación que incluye la función de la pregunta y que es portátil entre diferentes familias de microcontroladores:
#include <stdint.h>
uint32_t MyFunction(uint32_t crc, uint8_t *buffer, uint16_t length) {
uint16_t i;
uint16_t j;
for (i = 0; i < length; i++) {
crc = crc ^ *buffer++;
for (j = 0; j < 8; j++) {
if (crc & 1)
crc = (crc >> 1) ^ 0xEDB88320;
else
crc = crc >> 1;
}
}
return crc;
}
int main(int argc, char** argv) {
uint8_t data[] = "ABCDEF";
uint32_t ret = 0;
ret = MyFunction(ret, data, 6);
while(1);
}
Como se señaló en los comentarios sobre la otra pregunta, el valor de la variable i
nunca se usa directamente, solo se compara con length
. Por lo tanto, podríamos reescribirlo de la siguiente manera:
uint32_t MyFunction(uint32_t crc, uint8_t *buffer, uint16_t length) {
uint16_t j;
while (length--) {
// .. do work ..
}
return crc;
}
Para fines de comparación, utilicé la versión 4.7.2 de avr-gcc (dirigida a atmega8) y XC8 1.21 de Microchip (dirigida a PIC 18F). Para XC8 habilité optimizaciones PRO, para avr-gcc los argumentos dados fueron: avr-gcc -g -c -Os -Wall -mmcu=atmega8 test.c -o test-Os
.
Tenga en cuenta que es importante verificar que ret
no esté optimizado porque el compilador cree que no se usa. Ambas variaciones del código C generan el mismo ensamblado a partir de avr-gcc. Sin embargo, XC8 no nota la variable no utilizada, por lo tanto, el código que usa un while(..)
bucle se compila en 4 bytes más pequeños con 2 bytes de RAM guardados.
Como se señaló en un comentario sobre esta pregunta, también sería más eficiente hacer j
un uint8_t
, ya que la entrada nunca tendrá más de 8 bits de ancho. Las pruebas en XC8 muestran que hacer este cambio ahorra otros 8 bytes de espacio de programa y 1 byte de RAM. También reduce la salida generada con avr-gcc en 4 bytes y un byte de registro.
En conclusión , siempre vale la pena darle al compilador la mejor oportunidad de generar un buen código , en este caso al no usar variables adicionales innecesarias y al usar la clase de almacenamiento más pequeña posible. Algunos compiladores de optimización se las arreglarán mejor que otros, pero ambos funcionarán bien si el código C de entrada se ha pensado bien.
Este tipo de cosas se pueden reescribir en muchos procesadores en una pequeña rutina de lenguaje ensamblador que se ejecutará mucho más rápido que cualquier posible implementación de C. En un PIC, por ejemplo, el cálculo de CRC podría escribirse usando 72 instrucciones si _crc está en el banco seleccionado actualmente (código para PIC de la serie 16F)
movf _crc+1,w
btfss _crc,0
xorlw 0xNN ; Would need to figure out proper value
btfss _crc,1
xorlw 0xNN ; Would need to figure out proper value
.. six more such instruction pairs
movwf btemp+0 ; LSB of return
movf _crc+2,w
btfss _crc,0
xorlw 0xNN ; Would need to figure out proper value
btfss _crc,1
xorlw 0xNN ; Would need to figure out proper value
.. six more such instruction pairs
movwf btemp+1 ; Next byte of return
movf _crc+3,w
.. eight more instruction pairs
movwf btemp+2,w
movlw 0
.. eight more instruction pairs
movwf btemp+3
No es lo más compacto del mundo, pero actualizaría el CRC32 para un byte entrante en 72 ciclos. Alternativamente, si uno estuviera usando una parte de la serie 18F y pudiera ahorrar el espacio del código, uno podría usar tablas por valor de 1 Kbyte (organizadas como cuatro piezas de 256 bytes alineadas con la página). El código sería entonces algo como:
movf _crc,w,b
movwf _TBLPTRL,c
movlw TabUpperAddress
movwf _TBLPTRU,c
movlw TabHighAddress
movwf _TBLPTRH,c
tblrd *
movff _TABLAT,btemp+3
incf _TBLPTRH,c
tblrd *
xorwf _crc+1,w,b
movff _TABLAT,btemp+0
incf _TBLPTRH,c
tblrd *
xorwf _crc+2,w,b
movff _TABLAT,btemp+1
incf _TBLPTRH,c
tblrd *
xorwf _crc+3,w,b
movff _TABLAT,btemp+2
Un poco más rápido, pero requeriría 1 Kbytes de tablas de datos además del código en sí.
olin lathrop
Renán
apalopohapa
David
David
David