¿Debo refactorizar mi código C para optimizarlo para un microcontrolador incorporado?

3

A continuación hay un código implementado en un microcontrolador de 8 bits.

El siguiente comentario se publicó en otra pregunta :

  

Como su código no usa la variable i , ¿por qué no solo while(length--)   en lugar del primer for loop?

Se sugiere cambiar el bucle for(..) para un while(..) , ¿esto supondrá una diferencia práctica en cuanto a la optimización del código para un microcontrolador incorporado?

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 subido por votación pero sentí que debería respaldarlo con algunas pruebas reales. Otras respuestas serían útiles, pero esto fue pensado como una prueba genérica de un punto en lugar de una pregunta específica sobre el algoritmo.

    
pregunta David

3 respuestas

3

Lo primero a considerar es qué es necesario optimizar. ¿El código necesita tener menos memoria de programa o debería ejecutarse más rápido? A veces, ambas cosas pueden lograrse reduciendo el número de instrucciones, sin embargo, esto depende del microcontrolador subyacente y del número 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ñala en los comentarios en 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é avr-gcc versión 4.7.2 (apuntando a atmega8) y XC8 1.21 de Microchip (apuntando a PIC 18F). Para XC8, habilité las 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 piensa que no está en uso. Ambas variaciones en el código C generan el mismo ensamblaje desde avr-gcc. Sin embargo, XC8 no se da cuenta de la variable no utilizada, por lo tanto, el código que utiliza un bucle while(..) se compila a 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 a 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, no utilizando variables innecesarias adicionales y utilizando la clase de almacenamiento más pequeña posible. Algunos compiladores optimizados se comportarán mejor que otros, pero ambos funcionan bien si el código C de entrada ha sido bien pensado.

    
respondido por el David
8

Las tres reglas de optimización clásicas:

  1. no.
  2. todavía no.
  3. perfil antes de optimizar.

En primer lugar, no se pierde nada con escribir código limpio y ordenado, 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.

    
respondido por el RedGrittyBrick
1

Este tipo de cosas puede reescribirse 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 del CRC podría escribirse usando 72 instrucciones si _crc está en el banco actualmente seleccionado (código para los 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 la cosa más compacta del mundo, pero actualizaría el CRC32 para un byte entrante en 72 ciclos. Alternativamente, si uno usara una parte de la serie 18F y pudiera ahorrar espacio de código, podría usar tablas de 1Kbyte (organizadas en cuatro partes de 256 bytes alineadas a la página). El código sería algo así 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 1Kbytes de tablas de datos además del código en sí.

    
respondido por el supercat

Lea otras preguntas en las etiquetas