fbpixel
Etiquetas:

Optimización del código C para sistemas empotrados

La optimización del código C es necesaria para los sistemas empotrados porque, para ahorrar en costes de hardware, se planifica lo mínimo en términos de interfaz, memoria y capacidad de cálculo. En cambio, en un ordenador fijo se prefieren tiempos de ejecución bajos para no sobrecargar la CPU. La optimización tendrá dos objetivos contrapuestos

  • reducir la cantidad de memoria utilizada por un programa
  • aumentar la velocidad de ejecución de las funciones del programa

Un programa puede ser lo más rápido posible o lo más pequeño posible, pero no ambas cosas. Optimizar un criterio puede repercutir mucho en el otro.

A menudo, el compilador se encarga de optimizar el código, pero es esencial optimizarlo manualmente. Por lo general, nos concentramos en optimizar las secciones críticas del código y dejamos que el compilador se encargue del resto.

simplified-embedded-system-block-diagram Pruebas y optimización del código C/C++ con GNU

Aunque hay buenas prácticas a tener en cuenta a la hora de desarrollar código, éste sólo debe optimizarse cuando sea estrictamente necesario (límite de memoria, ejecución lenta). Por encima de todo, el código debe ser legible, mantenible y funcional.

Mejorar la eficacia del código

Funciones en línea

La palabra clave inline se utiliza para indicar al compilador que sustituya una llamada a la función por el código de la función. Cuando una función está presente en pocas secciones de código pero se llama a ella un gran número de veces, transformarla en una función inline puede mejorar el rendimiento de ejecución del código.

Tablas de consulta

Las búsquedas pueden utilizarse para sustituir funciones que requieren cálculos complicados por una simple asociación de variables. Por ejemplo, puede sustituir una función sin() o secciones swtich por tablas de opción múltiple.

Código ensamblador manual

Un compilador transforma el código C en código ensamblador optimizado. Cuando la función es crítica, un desarrollador experimentado puede crear su propio código ensamblador.

Reducción del tamaño del código (ROM)

Tamaño y tipo variables

Elegir correctamente la estructura, el tipo y el tamaño de la variable necesaria para almacenar los datos mejora considerablemente el rendimiento del código.

Declaración Goto

La función goto evita los algoritmos de árbol complicados. Esto dificulta la lectura del código y puede ser fuente de más errores.

Evite las bibliotecas estándar

Por lo general, las bibliotecas estándar son muy grandes y exigen muchos cálculos, porque intentan cubrir todos los casos. Desarrollar tus propias funciones para satisfacer tus necesidades específicas es una buena forma de optimizar tu código C#.

Reducir el uso de memoria (RAM)

Palabras clave const y static

Una variable estática es una variable a la que sólo se puede acceder en el contexto de una función, pero cuyo estado se mantiene entre llamadas a la función. Esto limita el uso de variables globales.

Probar el rendimiento del código

Medición del tiempo de ejecución de una sección de código mediante la biblioteca time.h

#include <iostream>
#include <time.h>

clock_t start, end;
double cpu_time_used;
int main()
{
    std::cout << "Hello World" << std::endl;
    start = clock();
    for(;i<0xffffff;i++);
    end = clock();
    cpu_time_used = ((double) (end - start)) / CLOCKS_PER_SEC;
    printf("Execution time: %f seconds\n", cpu_time_used);
    return 0;
}

Uso de gprof

La herramienta gprof suministrada con el compilador proporciona tiempos de ejecución para diferentes secciones de código o funciones.

compilar con el indicador -pg para que el código genere un archivo gmon.out

g++ -g -Wall -no-pie -pg helloworld.cpp -o helloworld --include=include/utils.cpp  #compile for profiling
./helloworld  #execute and write gnom.out
gprof -b helloworld.exe gmon.out > perfo.txt  #translate gnom.out

Ejemplo de salida del fichero perfo.txt: se obtiene una tabla que contiene el tiempo de ejecución acumulado sobre el tiempo de ejecución total, el número de llamadas y el tiempo de ejecución medio.

Flat profile:

Each sample counts as 0.01 seconds.
  %   cumulative   self              self     total           
 time   seconds   seconds    calls   s/call   s/call  name    
 37.21      4.26     4.26        2     2.13     3.95  func1()
 31.79      7.90     3.64        1     3.64     3.64  new_func1()
 30.83     11.43     3.53                             func2()
  0.17     11.45     0.02                             main
  0.00     11.45     0.00        1     0.00     0.00  count_to(int, int)

Uso de la memoria

La opción de compilación -stats proporciona estadísticas generales sobre el código

g++ -g -Wall -no-pie -pg helloworld.cpp -o helloworld --include=include/utils.cpp --stats

resultado

(No per-node statistics)
Type hash: size 32749, 23922 elements, 1.178881 collisions
DECL_DEBUG_EXPR  hash: size 1021, 0 elements, 0.000000 collisions
DECL_VALUE_EXPR  hash: size 1021, 18 elements, 0.031250 collisions
decl_specializations: size 8191, 6113 elements, 1.427515 collisions
type_specializations: size 8191, 3619 elements, 1.569889 collisions

******
time in header files (total): 0.862000 (38%)
time in main file (total): 1.416000 (62%)
ratio = 0.608757 : 1

******
time in ./include/utils.cpp: 0.002000 (0%)
time in <built-in>: 0.006000 (0%)
time in <command-line>: 0.000000 (0%)
time in <top level>: 0.009000 (0%)

Las opciones -fstack-usage y -Wstack-usage se utilizan para comprobar el uso de la memoria de pila en tiempo de compilación.

g++ -g -Wall -no-pie -pg helloworld.cpp -o helloworld --include=include/utils.cpp -fstack-usage
g++ -g -Wall -no-pie -pg helloworld.cpp -o helloworld --include=include/utils.cpp -Wstack-usage=32

El segundo comprueba el tamaño de memoria de pila esperado para un valor determinado (32).

In file included from <command-line>:
./include/utils.cpp: In function 'void count_to(int, int)':
./include/utils.cpp:6:6: warning: stack usage is 64 bytes [-Wstack-usage=]
    6 | void count_to(int n, int delay)
      |      ^~~~~~~~
helloworld.cpp: In function 'void new_func1()':
helloworld.cpp:7:6: warning: stack usage is 64 bytes [-Wstack-usage=]
    7 | void new_func1(void)
      |      ^~~~~~~~~
helloworld.cpp: In function 'void func1()':
helloworld.cpp:17:6: warning: stack usage is 64 bytes [-Wstack-usage=]
   17 | void func1(void)
      |      ^~~~~
helloworld.cpp: In function 'void func2()':
helloworld.cpp:28:13: warning: stack usage is 64 bytes [-Wstack-usage=]
   28 | static void func2(void)
      |             ^~~~~
helloworld.cpp: In function 'int main()':
helloworld.cpp:40:5: warning: stack usage is 96 bytes [-Wstack-usage=]
   40 | int main(void)

Simulación de la huella de memoria en hardware específico

Es posible simular una determinada pieza de hardware con un script de enlace personalizado

myldscript.lds

MemoryStart AddressSize
Internal Flash0x00000000256 Kbytes
Internal SRAM0x2000000032 Kbytes
MEMORY
{
  rom      (rx)  : ORIGIN = 0x00000000, LENGTH = 0x00040000
  ram      (rwx) : ORIGIN = 0x20000000, LENGTH = 0x00008000
}

STACK_SIZE = 0x2000;

/* Section Definitions */
SECTIONS
{
    .text :
    {
        KEEP(*(.vectors .vectors.*))
        *(.text*)
        *(.rodata*)
    } > rom

    /* .bss section which is used for uninitialized data */
    .bss (NOLOAD) :
    {
        *(.bss*)
        *(COMMON)
    } > ram

    .data :
    {
        *(.data*);
    } > ram AT >rom

    /* stack section */
    .stack (NOLOAD):
    {
        . = ALIGN(8);
        . = . + STACK_SIZE;
        . = ALIGN(8);
    } > ram

    _end = . ;
}
gcc -g -c helloworld.cpp # compile object file
ld -o helloworld -T ldscript.lds helloworld.o --print-memory-usage #compile with specific link script
Memory region         Used Size  Region Size  %age Used
             rom:       12704 B       256 KB      4.85%
             ram:        4128 B        32 KB     12.60%

Fuentes