tips - Optimizando el espacio en lugar de la velocidad en C++
tips para programar en c (16)
Algunos pocos obvios
- Si la velocidad no es crítica, ejecute el código directamente desde el flash.
- Declarar tablas de datos constantes usando
const
. Esto evitará que los datos se copien de flash a RAM - Empaque las tablas de datos grandes estrechamente con los tipos de datos más pequeños y en el orden correcto para evitar el relleno.
- Use compresión para grandes conjuntos de datos (siempre que el código de compresión no supere los datos)
- Desactive el manejo de excepciones y RTTI.
- ¿Alguien mencionó usar -Os? ;-)
Doblando el conocimiento en datos
Una de las reglas de la filosofía de Unix puede ayudar a que el código sea más compacto:
Regla de representación: dobla el conocimiento en los datos para que la lógica del programa pueda ser estúpida y robusta.
No puedo contar cuántas veces he visto lógica de ramificación elaborada, que abarca muchas páginas, que podría haber sido plegada en una tabla compacta de reglas, constantes y punteros de función. Las máquinas de estado a menudo se pueden representar de esta manera (patrón de estado). El patrón de comando también se aplica. Se trata de los estilos de programación declarativos vs imperativos.
Códigos de registro + datos binarios en lugar de texto
En lugar de registrar texto sin formato, registre códigos de eventos y datos binarios. Luego use un "libro de frases" para reconstituir los mensajes de eventos. Los mensajes en el libro de frases incluso pueden contener especificadores de formato de estilo printf, de modo que los valores de datos de evento se visualicen prolijamente dentro del texto.
Minimiza la cantidad de hilos
Cada hilo necesita su propio bloque de memoria para una pila y TSS. Cuando no necesite prioridad, considere hacer que sus tareas se desarrollen de manera cooperativa dentro del mismo hilo ( tareas múltiples cooperativas ).
Usa pools de memoria en lugar de acaparamiento
Para evitar la fragmentación del montón, a menudo he visto módulos separados que acumulan grandes memorias intermedias estáticas para su propio uso, incluso cuando la memoria solo se requiere ocasionalmente. En su lugar, se puede usar un conjunto de memoria, por lo que la memoria solo se usa "a pedido". Sin embargo, este enfoque puede requerir un análisis cuidadoso e instrumentación para garantizar que las agrupaciones no se agoten en el tiempo de ejecución.
Asignación dinámica solo en la inicialización
En sistemas integrados donde solo una aplicación se ejecuta indefinidamente, puede usar la asignación dinámica de una manera sensata que no conduzca a la fragmentación: simplemente asigne una vez dinámicamente en sus diversas rutinas de inicialización, y nunca libere la memoria. reserve()
sus contenedores a la capacidad correcta y no los deje crecer automáticamente. Si necesita asignar / buffers de datos con frecuencia (por ejemplo, para paquetes de comunicación), utilice grupos de memoria. Una vez incluso extendí los tiempos de ejecución de C / C ++ para que abortara mi programa si algo intentaba asignar dinámicamente la memoria después de la secuencia de inicialización.
Cuando dices "optimización", la gente tiende a pensar "velocidad". Pero, ¿qué ocurre con los sistemas integrados en los que la velocidad no es tan crítica, pero la memoria es una limitación importante? ¿Cuáles son algunas pautas, técnicas y trucos que se pueden utilizar para eliminar esos kilobytes adicionales en ROM y RAM? ¿Cómo funciona un código de "perfil" para ver dónde está la saturación de la memoria?
PS Uno podría argumentar que optimizar "prematuramente" el espacio en sistemas integrados no es tan malo, porque se deja más espacio para el almacenamiento de datos y la característica de fluencia. También le permite reducir los costos de producción de hardware porque su código puede ejecutarse en ROM / RAM más pequeñas.
¡También son bienvenidas las referencias a los artículos y libros de PPS!
PPPS Estas preguntas están estrechamente relacionadas: 404615 , 1561629
Aquí hay un libro sobre el tema Small Memory Software: Patrones para sistemas con memoria limitada .
Como con todas las optimizaciones, primero optimice los algoritmos, en segundo lugar optimice el código y los datos, finalmente optimice el compilador.
No sé lo que hace tu programa, así que no puedo aconsejar sobre algoritmos. Muchos otros han escrito sobre el compilador. Entonces, aquí hay algunos consejos sobre código y datos:
- Elimine la redundancia en su código. Cualquier código repetido que tenga tres o más líneas, repetido tres veces en su código, debe cambiarse a una llamada a función.
- Elimine la redundancia en sus datos. Encuentre la representación más compacta: combine datos de solo lectura y considere usar códigos de compresión.
- Ejecute el código a través de un generador de perfiles regular; eliminar todo el código que no se utiliza.
Compilar en VS con / Os. A menudo, esto es incluso más rápido que optimizar la velocidad de todos modos, porque el tamaño del código más pequeño == menos paginación.
Comdat plegable debe estar habilitado en el enlazador (es por defecto en compilaciones de lanzamiento)
Tenga cuidado con el embalaje de la estructura de datos; a menudo el tiempo esto da como resultado que el compilador genere más código (== más memoria) para generar el ensamblaje para acceder a la memoria no alineada. Usar 1 bit para una bandera booleana es un ejemplo clásico.
Además, tenga cuidado al elegir un algoritmo de memoria eficiente sobre un algoritmo con un mejor tiempo de ejecución. Aquí es donde entran las optimizaciones prematuras.
Genere un archivo de mapa de su enlazador. Mostrará cómo se asigna la memoria. Este es un buen comienzo al optimizar el uso de la memoria. También mostrará todas las funciones y cómo se presenta el espacio de código.
Hay muchas cosas que puede hacer para reducir sus huellas de memoria, estoy seguro de que la gente ha escrito libros sobre el tema, pero algunas de las más importantes son:
Opciones del compilador para reducir el tamaño del código (incluidas opciones de empaque y alineación)
Opciones de vinculador para pelar el código muerto
Si está cargando desde el flash (o ROM) a la ejecución de RAM (en lugar de ejecutar desde el flash), utilice una imagen flash comprimida y descomprímala con su gestor de arranque.
Use la asignación estática: un montón es una forma ineficiente de asignar memoria limitada, y si puede fallar debido a la fragmentación si está restringida.
Herramientas para encontrar la pila de marca de agua alta (normalmente llenan la pila con un patrón, ejecutan el programa y luego ven dónde queda el patrón), para que pueda establecer el tamaño de la pila de manera óptima
Y, por supuesto, optimizar los algoritmos que utiliza para la huella de memoria (a menudo a expensas de la velocidad)
Junto con eso, todos los demás dijeron, me gustaría agregar que no use funciones virtuales porque con funciones virtuales se debe crear una VTable que pueda ocupar quién sabe cuánto espacio.
También ten cuidado con las excepciones. Con gcc, no creo que haya un tamaño creciente para cada bloque de try-catch (excepto para 2 call
función para cada try-catch), pero hay una función de tamaño fijo que debe estar vinculada en la que podría estar desperdiciando preciosas bytes
La optimización es un término popular pero a menudo técnicamente incorrecto. Literalmente significa hacer lo óptimo. Tal condición nunca se logra realmente para la velocidad o el tamaño. Simplemente podemos tomar medidas para avanzar hacia la optimización.
Muchas (pero no todas) las técnicas utilizadas para avanzar hacia el tiempo mínimo para un resultado informático sacrifican el requisito de memoria, y muchas (pero no todas) las técnicas utilizadas para avanzar hacia el requisito mínimo de memoria alargan el tiempo de resultado.
La reducción de los requisitos de memoria equivale a un número fijo de técnicas generales. Es difícil encontrar una técnica específica que no encaje perfectamente en uno o más de estos. Si hicieras todos ellos, tendrías algo muy cercano al requisito de espacio mínimo para el programa, si no el mínimo absoluto posible. Para una aplicación real, podría tomar un equipo de programadores experimentados mil años para hacerlo.
- Elimine toda la redundancia de los datos almacenados, incluidos los productos intermedios.
- Elimine toda la necesidad de almacenar datos que podrían transmitirse en su lugar.
- Asigne solo la cantidad de bytes necesarios, nunca más.
- Eliminar todos los datos no utilizados.
- Eliminar todas las variables no utilizadas.
- Datos gratuitos tan pronto como ya no sean necesarios.
- Elimine todos los algoritmos y ramas no utilizados dentro de los algoritmos.
- Encuentre el algoritmo que se representa en la unidad de ejecución de tamaño mínimo.
- Elimine todo el espacio no utilizado entre los elementos.
Esta es una visión de la ciencia de la computación del tema, no del desarrollador.
Por ejemplo, empaquetar una estructura de datos es un esfuerzo que combina (3) y (9) arriba. La compresión de datos es una manera de alcanzar al menos parcialmente (1) arriba. La reducción de la sobrecarga de las construcciones de programación de alto nivel es una forma de lograr algún progreso en (7) y (8). La asignación dinámica es un intento de explotar un entorno multitarea para emplear (3). Las advertencias de compilación, si están activadas, pueden ayudar con (5). Los destructores intentan ayudar con (6). Los zócalos, las corrientes y las tuberías se pueden usar para lograr (2). Simplificar un polinomio es una técnica para ganar terreno en (8).
La comprensión del significado de nueve y las diversas formas de lograrlos es el resultado de años de aprendizaje y comprobación de mapas de memoria resultantes de la compilación. Los programadores integrados a menudo los aprenden más rápidamente debido a la memoria limitada disponible.
El uso de la opción -Os en un compilador gnu solicita al compilador que intente encontrar patrones que puedan transformarse para lograrlos, pero el -Os es un marcador agregado que activa varias características de optimización, cada una de las cuales intenta realizar transformaciones para llevar a cabo una de las 9 tareas anteriores.
Las directivas de compilación pueden producir resultados sin esfuerzo del programador, pero los procesos automatizados en el compilador raramente corrigen los problemas creados por la falta de conocimiento en los redactores del código.
Mi experiencia de un entorno de memoria incrustado extremadamente limitado:
- Use almacenamientos intermedios de tamaño fijo. No use punteros ni asignación dinámica porque tienen demasiada sobrecarga.
- Use el tipo de datos int más pequeño que funcione.
- Nunca use recursión. Siempre usa bucle.
- No pase muchos parámetros de funciones. Usa globals en su lugar. :)
No tengas miedo de escribir ''pequeños idiomas'' dentro de tu programa. A veces, una tabla de cadenas y un intérprete pueden hacer MUCHO. Por ejemplo, en un sistema en el que he trabajado, tenemos muchas tablas internas a las que hay que acceder de varias formas (en bucle, lo que sea). Tenemos un sistema interno de comandos para hacer referencia a las tablas que forma una especie de lenguaje a medio camino que es bastante compacto para lo que se obtiene.
¡Pero ten cuidado! Sepa que está escribiendo tales cosas (yo mismo escribí una accidentalmente), y DOCUMENTE lo que está haciendo. Los desarrolladores originales NO parecen haber sido conscientes de lo que estaban haciendo, por lo que es mucho más difícil de administrar de lo que debería ser.
Ok, ya se mencionó la mayoría, pero aquí está mi lista de todos modos:
- Aprenda lo que su compilador puede hacer. Lea la documentación del compilador, experimente con ejemplos de código. Compruebe la configuración.
- Verifique el código generado en el nivel de optimización del objetivo. A veces los resultados son sorprendentes y, a menudo resulta que la optimización realmente ralentiza las cosas (o simplemente toma demasiado espacio).
- elija el modelo de memoria adecuado. Si se dirige a un sistema muy pequeño y ajustado, un modelo de memoria grande o grande podría no ser la mejor opción (pero por lo general es lo más fácil de programar para ...)
- Prefiere la asignación estática . Utilice la asignación dinámica solo en el inicio o en el búfer asignado de forma estática (grupo o búfer estático de tamaño de instancia máximo).
- Use tipos de datos de estilo C99 . Utilice el tipo de datos más pequeño suficiente para los tipos de almacenamiento. Las variables locales como las variables de bucle son a veces más eficientes con tipos de datos "rápidos".
- Seleccione candidatos en línea . Algunos parámetros de función pesada con cuerpos relativamente simples son mejores cuando están en línea. O considere pasar la estructura de los parámetros. Los Globals también son una opción, pero ten cuidado: las pruebas y el mantenimiento pueden ser difíciles si alguien no está lo suficientemente disciplinado.
- Utilice la palabra clave const bien, tenga en cuenta las implicaciones de inicialización de la matriz.
- Archivo de mapa , idealmente también con tamaños de módulo. Verifique también qué está incluido en crt (¿es realmente necesario?).
- La recursividad solo dice que no (espacio de pila limitado)
- Números flotantes de puntos : prefieren matemáticas de punto fijo. Tiende a incluir y invocar una gran cantidad de código (incluso para una simple suma o multiplicación).
- C ++ debes saber C ++ MUY BIEN. Si no lo hace, programe sistemas integrados con restricciones en C, por favor. Aquellos que se atreven deben tener cuidado con todas las construcciones avanzadas de C ++ (herencia, plantillas, excepciones, sobrecarga, etc.). Considere que el código HW es más bien Super-C y C ++ se usa donde cuenta: en lógica de alto nivel, GUI, etc.
- Deshabilite lo que no necesite en la configuración del compilador (ya sea partes de bibliotecas, construcciones de lenguaje, etc.)
Por último, aunque no menos importante, mientras busca el tamaño de código más pequeño posible, no se exceda . Tenga cuidado también con el rendimiento y la capacidad de mantenimiento. El código sobre-optimizado tiende a decaer muy rápidamente.
Primero, dile a tu compilador que optimice el tamaño del código. GCC tiene la bandera de -Os
para esto.
Todo lo demás está en el nivel algorítmico: use herramientas similares a las que tendría para encontrar fugas de memoria, pero busque los allocs y los liberes que pueda evitar.
También eche un vistazo al empaque de la estructura de datos comúnmente utilizada: si puede quitar un byte o dos de ellos, puede reducir sustancialmente el uso de la memoria.
Si está buscando una buena forma de perfilar el uso del montón de su aplicación, consulte la herramienta de valgrind massif . Te permitirá tomar instantáneas del perfil de uso de la memoria de tu aplicación a lo largo del tiempo, y luego podrás utilizar esa información para ver mejor dónde está la "fruta que cuelga más bajo" y apuntar tus optimizaciones en consecuencia.
Tenga en cuenta el costo de implementación de algunas funciones de C ++, como tablas de funciones virtuales y operadores sobrecargados que crean objetos temporales.
en la parte superior lo que otros sugieren:
Limite el uso de las características de C ++, escriba como en ANSI C con extensiones menores. Las plantillas estándar (std: :) usan un gran sistema de asignación dinámica. Si puedes, evita las plantillas por completo. Si bien no son intrínsecamente dañinos, hacen que sea demasiado fácil generar muchos códigos de máquina a partir de un par de instrucciones simples, limpias y elegantes de alto nivel. Esto fomenta la escritura de una manera que, a pesar de todas las ventajas del "código limpio", tiene mucha memoria.
Si debe usar plantillas, escriba las suyas o use las diseñadas para uso integrado, pase tamaños fijos como parámetros de plantilla y escriba un programa de prueba para que pueda probar su plantilla Y verifique su salida -S para asegurarse de que el compilador no esté generando ensamblaje horrible código para instanciarlo.
Alinee sus estructuras a mano, o use #pragma pack
{char a; long b; char c; long d; char e; char f; } //is 18 bytes,
{char a; char c; char d; char f; long b; long d; } //is 12 bytes.
Por el mismo motivo, utilice una estructura de almacenamiento de datos global centralizada en lugar de variables locales estáticas dispersas.
Equilibre de forma inteligente el uso de malloc () / estructuras nuevas y estáticas.
Si necesita un subconjunto de funcionalidad de una determinada biblioteca, considere escribir la suya propia.
Desenrollar los bucles cortos.
for(i=0;i<3;i++){ transform_vector[i]; }
es más largo que
transform_vector[0];
transform_vector[1];
transform_vector[2];
No hagas eso por más tiempo.
Empaqueta múltiples archivos juntos para permitir que el compilador haga cortas las funciones y realice varias optimizaciones. Linker no puede.