¿Cómo exprimir el código para obtener más Flash y RAM? [cerrado]


14

He trabajado en el desarrollo de una función en un producto particular nuestro. Ha habido una solicitud para portar la misma característica a otro producto. Este producto se basa en un microcontrolador M16C, que tradicionalmente tiene 64K Flash y 2k de RAM.

Es un producto maduro y, por lo tanto, solo le quedan 132 Bytes de Flash y 2 Bytes de RAM.

Para portar la característica solicitada (la característica misma ha sido optimizada), necesito 1400 bytes de Flash y ~ 200 Bytes de RAM.

¿Alguien tiene alguna sugerencia sobre cómo recuperar estos bytes por compactación de código? ¿Qué cosas específicas busco cuando intento compactar el código de trabajo ya existente?

Cualquier idea será realmente apreciada.

Gracias.


1
Gracias a todos por las sugerencias. Los mantendré actualizados con mi progreso y enumeraré los pasos que funcionaron y los que no.
IntelliChick

Ok, aquí están las cosas que he probado que funcionaron: versiones de compilador mejoradas. La optimización había mejorado drásticamente, lo que me dio aproximadamente 2K de Flash. Revisó los archivos de la lista para verificar la funcionalidad redundante y no utilizada (heredada debido a la base de código común) para el producto en particular y ganó un poco más de Flash.
IntelliChick

Para RAM hice lo siguiente: Revisé el archivo de mapa para verificar las funciones / módulos que usaban más RAM. Encontré una función realmente pesada (12 canales, cada uno con una cantidad fija de memoria asignada), de código heredado, entendí lo que estaba tratando de lograr y optimicé el uso de RAM, al compartir información entre los canales que era común. Esto me dio ~ 200 bytes que necesitaba.
IntelliChick

Si tiene archivos ascii, puede usar una compresión de 8 a 7 bits. Le ahorra 12.5%. Usar un archivo zip requeriría más código para comprimirlo y descomprimirlo que simplemente dejarlo.
Sparky256

Respuestas:


18

Tiene un par de opciones: la primera es buscar código redundante y moverlo a una sola llamada para deshacerse de la duplicación; El segundo es eliminar la funcionalidad.

Eche un buen vistazo a su archivo .map y vea si hay funciones de las que pueda deshacerse o reescribir. También asegúrese de que las llamadas a la biblioteca que se están utilizando sean realmente necesarias.

Ciertas cosas como la división y las multiplicaciones pueden traer mucho código, pero el uso de cambios y un mejor uso de las constantes puede hacer que el código sea más pequeño. También eche un vistazo a cosas como las constantes de cadena y printfs. Por ejemplo cadaprintf consumirá su rom, pero es posible que pueda tener un par de cadenas de formato compartidas en lugar de repetir esa cadena constantemente una y otra vez.

En cuanto a la memoria, vea si puede deshacerse de los globales y usar autos en una función. También evite tantas variables en la función principal como sea posible, ya que estas consumen memoria al igual que lo hacen los globales.


1
Gracias por las sugerencias, definitivamente puedo probar la mayoría de ellas, excepto la de las constantes de cadena. Es puramente un dispositivo integrado, sin interfaz de usuario y, por lo tanto, no hay llamadas a printf () dentro del código. Esperando que esas sugerencias me ayuden a obtener mis 1400 Bytes Flash / 200 bytes de RAM que necesito.
IntelliChick

1
@IntelliChick, se sorprendería de cuántas personas usan printf () dentro de un dispositivo integrado para imprimir, ya sea para depurar o enviar a un periférico. Parece que lo sabes mejor que esto, pero si alguien ha escrito un código sobre el proyecto antes que tú, no estaría de más comprobarlo.
Kellenjb

55
Y solo para ampliar mi comentario anterior, también se sorprendería de cuántas personas agregan declaraciones de depuración, pero nunca las eliminan. Incluso las personas que hacen #ifdefs todavía se vuelven perezosas a veces.
Kellenjb

1
¡Genial gracias! He heredado esta base de código, así que definitivamente voy a buscarlas. Los mantendré informados, sobre el progreso, e intentaré hacer un seguimiento de cuántos bytes de memoria o Flash gané haciendo qué, solo como referencia para cualquier otra persona que pueda necesitar hacer esto en el futuro.
IntelliChick

Solo una pregunta sobre esto: ¿qué pasa con la función anidada que llama saltos de capa a capa? ¿Cuánto gasto adicional agrega? ¿Es mejor mantener la modularidad al tener llamadas a múltiples funciones o reducir las llamadas a funciones y guardar algunos bytes? ¿Y eso es significativo?
IntelliChick

8

Siempre vale la pena mirar la salida del archivo de lista (ensamblador) para buscar cosas en las que su compilador particular es particularmente malo.

Por ejemplo, puede encontrar que las variables locales son muy caras, y si la aplicación es lo suficientemente simple como para que valga la pena el riesgo, mover algunos contadores de bucle a variables estáticas puede ahorrar mucho código.

O la indexación de matrices puede ser muy costosa pero las operaciones de puntero mucho más baratas. O viceversa.

Pero mirar el lenguaje ensamblador es el primer paso.


3
Es muy importante que sepa lo que hace su compilador. Deberías ver qué división hay en mi compilador. Hace llorar a los bebés (incluido yo mismo).
Kortuk

8

Las optimizaciones del compilador, por ejemplo, -Osen GCC ofrecen el mejor equilibrio entre la velocidad y el tamaño del código. Evite -O3, ya que puede aumentar el tamaño del código.


3
Si haces esto, ¡tendrás que volver a probar TODO! Las optimizaciones pueden hacer que el código de trabajo no funcione debido a nuevas suposiciones que hace el compilador.
Robert

@Robert, eso solo es cierto si usa declaraciones indefinidas: por ejemplo, a = a ++ se compilará de manera diferente en -O0 y -O3.
Thomas O

55
@Thomas no es cierto. Si tiene un bucle for para retrasar los ciclos de reloj, muchos optimizadores se darán cuenta de que no está haciendo nada y lo eliminarán. Este es solo un ejemplo.
Kellenjb

1
@ thomas O, también debe asegurarse de tener cuidado con las definiciones de funciones volátiles. Los optimizadores harán explotar a aquellos que piensan que conocen bien C pero no entienden las complejidades de las operaciones atómicas.
Kortuk

1
Todos los buenos puntos. Las funciones / variables volátiles, por definición, NO deben optimizarse. Cualquier optimizador que realice optimizaciones sobre este (incluido el tiempo de llamada y la alineación) está roto.
Thomas O

8

Para RAM, verifique el rango de todas sus variables: ¿está usando ints donde podría usar un char? ¿Son los amortiguadores más grandes de lo que necesitan ser?

La compresión de código depende mucho de la aplicación y del estilo de codificación. Sus cantidades restantes sugieren que tal vez el código ya se haya eliminado, aunque puede significar que queda poco.

También observe detenidamente la funcionalidad general: ¿hay algo que no se usa realmente y que se puede descartar?


8

Si es un proyecto antiguo pero el compilador se ha desarrollado desde entonces, podría ser que un compilador más reciente produzca un código más pequeño


Gracias Mike! He intentado esto en el pasado, ¡y el espacio ganado ya se ha utilizado! :) Subido del compilador IAR C 3.21d a 3.40.
IntelliChick

1
Subí una versión más y logré obtener un poco más de Flash para encajar en la función. Sin embargo, realmente estoy luchando con la RAM, que no ha cambiado. :(
IntelliChick

7

Siempre vale la pena consultar el manual del compilador para ver las opciones para optimizar el espacio.

Para gcc -ffunction-sections y -fdata-sectionscon el--gc-sections indicador de enlace son buenos para quitar el código muerto.

Aquí hay algunos otros excelentes consejos (orientados a AVR)


¿Esto realmente funciona? Los documentos dicen "Cuando especifique estas opciones, el ensamblador y el enlazador crearán objetos más grandes y archivos ejecutables y también serán más lentos". Entiendo que tener secciones separadas tiene sentido para un micro con secciones Flash y RAM. ¿Esta declaración en documentos no es aplicable a los microcontroladores?
Kevin Vermeer

Mi experiencia es que funciona bien para AVR
Toby Jaffey

1
Esto no funciona bien en la mayoría de los compiladores que he usado. Es como usar la palabra clave de registro. Puedes decirle al compilador que una variable entra en un registro, pero un buen optimizador lo hará mucho mejor que un humano (argumenta que, como algunos pueden decir, no se considera aceptable hacerlo en la práctica).
Kortuk

1
Cuando comienza a asignar ubicaciones, está obligando al compilador a colocar cosas en ciertas ubicaciones, muy importante para el código avanzado del cargador de arranque, pero horrible de manejar en el optimizador, ya que al tomar decisiones al respecto, está quitando un paso de optimización. podría hacer. En algunos compiladores lo diseñan para tener secciones para qué código se usa, este es un caso de decirle al compilador más información para comprender su uso, esto ayudará. Si el compilador no lo sugiere, no lo haga.
Kortuk

6

Puede examinar la cantidad de espacio de pila y espacio de almacenamiento dinámico que se asignan. Es posible que pueda recuperar una cantidad sustancial de RAM si alguno de los dos está sobreasignado.

Supongo que para un proyecto que cabe en 2k de RAM para comenzar, no hay asignación de memoria dinámica (uso de malloc,calloc , etc.). Si este es el caso, puede deshacerse de su montón por completo suponiendo que el autor original haya dejado algo de RAM asignada para el montón.

Debe tener mucho cuidado al reducir el tamaño de la pila, ya que esto puede causar errores que son muy difíciles de encontrar. Puede ser útil comenzar inicializando todo el espacio de la pila a un valor conocido (algo diferente a 0x00 o 0xff ya que estos valores ya ocurren comúnmente) y luego ejecutar el sistema durante un tiempo para ver cuánto espacio de la pila no se usa.


Estas son muy buenas opciones. Notaré que nunca deberías usar malloc en un sistema embebido.
Kortuk

1
@Kortuk Eso depende de su definición de incrustado y la tarea que se realiza
Toby Jaffey

1
@joby, sí, entiendo eso. En un sistema con 0 reinicios y la ausencia de un sistema operativo como Linux, Malloc puede ser muy malo.
Kortuk

No hay asignación de memoria dinámica, no hay lugar donde se esté usando malloc, calloc. También he verificado la asignación del montón, y ya se ha establecido en 0, por lo que no hay asignación del montón. El tamaño de pila actualmente asignado es de 254 bytes y el tamaño de pila de interrupción en 128 bytes.
IntelliChick el

5

¿Su código usa matemática de punto flotante? Es posible que pueda volver a implementar sus algoritmos usando matemática entera solamente, y eliminar los gastos generales de usar la biblioteca de coma flotante C. Por ejemplo, en algunas aplicaciones, funciones como seno, registro, exp pueden reemplazarse por aproximaciones polinómicas enteras.

¿Utiliza su código tablas de búsqueda grandes para algún algoritmo, como los cálculos de CRC? Puede intentar sustituir una versión diferente del algoritmo que calcula los valores sobre la marcha, en lugar de utilizar las tablas de búsqueda. La advertencia es que el algoritmo más pequeño es más lento, así que asegúrese de tener suficientes ciclos de CPU.

¿Su código tiene grandes cantidades de datos constantes, como tablas de cadenas, páginas HTML o gráficos de píxeles (iconos)? Si es lo suficientemente grande (digamos 10 kB), podría valer la pena implementar un esquema de compresión muy simple para reducir los datos y descomprimirlos sobre la marcha cuando sea necesario.


Hay 2 pequeñas tablas de búsqueda, ninguna de las cuales equivaldrá a 10K desafortunadamente. Y tampoco se están utilizando matemáticas de coma flotante. :( Sin embargo, gracias por las sugerencias. Son buenas.
IntelliChick

2

Puede intentar reorganizar para codificar mucho, a un estilo más compacto. Depende mucho de lo que esté haciendo el código. La clave es encontrar cosas similares y volver a implementarlas en términos mutuos. Un extremo sería utilizar un lenguaje de nivel superior, como Forth, con el que puede ser más fácil lograr una densidad de código más alta que en C o ensamblador.

Aquí está Forth para M16C .


2

Establecer el nivel de optimización del compilador. Muchos IDE tienen configuraciones que permiten optimizaciones de tamaño de código a expensas del tiempo de compilación (o tal vez incluso el tiempo de procesamiento en algunos casos). Pueden lograr la compactación de código volviendo a ejecutar su optimizador un par de veces, buscando patrones optimizables menos comunes y una gran cantidad de trucos que pueden no ser necesarios para la compilación casual / depuración. Por lo general, de manera predeterminada, los compiladores están configurados en un nivel medio de optimización. Excava en la configuración y deberías poder encontrar alguna escala de optimización basada en enteros.


1
Actualmente optimizado al máximo para el tamaño. :) Gracias por la sugerencia sin embargo. :)
IntelliChick

2

Si ya está utilizando un compilador de nivel profesional como IAR, creo que tendrá dificultades para obtener ahorros importantes mediante pequeños ajustes de código de bajo nivel; deberá buscar más para eliminar la funcionalidad o hacer grandes reescribe partes de una manera más eficiente. Tendrá que ser un codificador más inteligente que el que escribió la versión original ... En cuanto a la RAM, debe analizar detenidamente cómo se usa actualmente y ver si hay margen para superponer el uso de la misma RAM para diferentes cosas en diferentes momentos (los sindicatos son útiles para esto). Los tamaños de pila y pila predeterminados de IAR en los ARM / AVR que he tendido a ser demasiado generosos, por lo que estos serían lo primero que se debería considerar.


Gracias Mike El código ya está utilizando sindicatos en la mayoría de los lugares, pero echaré un vistazo a otros lugares, donde esto podría ayudar. También echaré un vistazo al tamaño de pila elegido y veré si eso puede optimizarse en absoluto.
IntelliChick

¿Cómo sé qué tamaño de pila es apropiado?
IntelliChick

2

Algo más para verificar (algunos compiladores en algunas arquitecturas copian constantes en la RAM), que generalmente se usa cuando el acceso a las constantes flash es lento / difícil (por ejemplo, AVR), por ejemplo, el compilador AVR de IAR requiere un calificador _ _flash para no copiar una constante en la RAM)


Gracias Mike Sí, ya lo había verificado: se llama la opción 'Constantes grabables' para el compilador M16C IAR C. Copia las constantes de ROM a RAM. Esta opción no está marcada para mi proyecto. ¡Pero un cheque realmente válido! Gracias.
IntelliChick el

1

Si su procesador no tiene soporte de hardware para un parámetro / pila local, pero el compilador intenta implementar una pila de parámetros en tiempo de ejecución de todos modos, y si su código no necesita ser reentrante, puede guardar el código espacio asignando estáticamente variables automáticas. En algunos casos, esto debe hacerse manualmente; en otros casos, las directivas del compilador pueden hacerlo. La asignación manual eficiente requerirá el intercambio de variables entre rutinas. Este intercambio debe realizarse con cuidado, para garantizar que ninguna rutina use una variable que otra rutina considere "en alcance", pero en algunos casos los beneficios del tamaño del código pueden ser significativos.

Algunos procesadores tienen convenciones de llamada que pueden hacer que algunos estilos de paso de parámetros sean más eficientes que otros. Por ejemplo, en los controladores PIC18, si una rutina toma un solo parámetro de un byte, se puede pasar en un registro; si se necesita más que eso, todos parámetros deben pasarse en la RAM. Si una rutina tomara dos parámetros de un byte, puede ser más eficiente "pasar" uno en una variable global y luego pasar el otro como parámetro. Con rutinas ampliamente utilizadas, los ahorros pueden sumar. Pueden ser especialmente significativos si el parámetro pasado a través de global es un indicador de un solo bit, o si generalmente tendrá un valor de 0 o 255 (ya que existen instrucciones especiales para almacenar un 0 o 255 en la RAM).

En el ARM, poner variables globales que se usan frecuentemente juntas en una estructura puede reducir significativamente el tamaño del código y mejorar el rendimiento. Si A, B, C, D y E son variables globales separadas, entonces el código que usa todas ellas debe cargar la dirección de cada una en un registro; Si no hay suficientes registros, puede ser necesario volver a cargar esas direcciones varias veces. Por el contrario, si forman parte de la misma estructura global de MyStuff, entonces el código que usa MyStuff.A, MyStuff.B, etc. simplemente puede cargar la dirección de MyStuff una vez. Gran victoria.


1

1.Si su código se basa en muchas estructuras, asegúrese de que los miembros de la estructura estén ordenados de los que ocupan menos memoria.

Ej: "uint32_t uint16_t uint8_t" en lugar de "uint16_t uint8_t uint32_t"

Esto asegurará una estructura mínima de relleno.

2. Use const para variables donde corresponda. Esto asegurará que esas variables estarán en ROM y no comerán RAM


1

Algunos trucos (quizás obvios) que he usado con éxito para comprimir el código de algunos clientes:

  1. Banderas compactas en campos de bits o máscaras de bits. Esto puede ser beneficioso ya que generalmente los booleanos se almacenan como enteros, lo que desperdicia memoria. Esto ahorrará RAM y ROM y, por lo general, el compilador no lo hace.

  2. Busque redundancia en el código y use bucles o funciones para ejecutar declaraciones repetidas.

  3. También he guardado algo de ROM reemplazando muchas if(x==enum_entry) <assignment>declaraciones de constantes con una matriz indexada, cuidando que las entradas de enumeración se puedan usar como índice de matriz


0

Si puede, use funciones en línea o macros de compilación en lugar de funciones pequeñas. Hay sobrecarga de tamaño y velocidad con argumentos de paso y tales que pueden remediarse haciendo que la función esté en línea.


1
Cualquier compilador decente debería hacer esto automáticamente para las funciones que se llaman solo una vez.
mikeselectricstuff

55
He encontrado que la alineación suele ser más útil para las optimizaciones de velocidad, y generalmente a costa de un mayor tamaño.
Craig McQueen

la alineación generalmente aumentará el tamaño del código, excepto con funciones triviales comoint get_a(struct x) {return x.a;}
Dmitry Grigoryev el

0

Cambie las variables locales para que tengan el mismo tamaño de sus registros de CPU.

Si la CPU es de 32 bits, use variables de 32 bits incluso si el valor máximo nunca superará 255. Si utilizó una variable de 8 bits, el compilador agregará código para enmascarar los 24 bits superiores.

El primer lugar que miraría es en las variables de bucle for.

for( i = 0; i < 100; i++ )

Esto puede parecer un buen lugar para una variable de 8 bits, pero una variable de 32 bits puede producir menos código.


Eso puede guardar el código pero comerá RAM.
mikeselectricstuff

Solo comerá RAM si esa llamada de función se encuentra en la rama más larga del rastreo de llamadas. De lo contrario, está reutilizando el espacio de pila que otra función ya necesita.
Robert

2
Por lo general, es cierto siempre que sea una variable local. Si tiene poca RAM, el tamaño de los vars globales, especialmente los arreglos, es un buen lugar para comenzar a buscar ahorros.
mikeselectricstuff

1
Otra posibilidad, curiosamente, es reemplazar las variables sin signo con signo. Si un compilador optimiza un corto sin signo a un registro de 32 bits, debe agregar código para asegurarse de que su valor se ajusta de 65535 a cero. Sin embargo, si el compilador optimiza un corto firmado para un registro, no se requiere dicho código. Debido a que no hay garantía de lo que sucederá si se incrementa un corto más allá de 32767, los compiladores no están obligados a emitir código para lidiar con eso. Por lo menos en dos compiladores ARM que he visto, el código corto firmado puede ser más pequeño que el código corto no firmado por ese motivo.
supercat
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.