punteros - ¿Cuáles son algunas de las mejores prácticas para reducir el uso de memoria en C?
manejo de memoria en c++ (15)
1) Antes de comenzar el proyecto, cree una manera de medir cuánta memoria está usando, preferiblemente en cada componente. De esta forma, cada vez que realizas un cambio puedes ver sus efectos sobre el uso de la memoria. No puedes optimizar lo que no puedes medir.
2) Si el proyecto ya está maduro y alcanza los límites de memoria (o se transfiere a un dispositivo con menos memoria), averigüe para qué está usando la memoria.
Mi experiencia ha sido que casi toda la optimización significativa cuando se repara una aplicación de gran tamaño proviene de un pequeño número de cambios: reducir el tamaño de la caché, eliminar algunas texturas (por supuesto, este es un cambio funcional que requiere un acuerdo de las partes interesadas, es decir, reuniones, no ser eficiente en términos de su tiempo), remuestrear el audio, reducir el tamaño inicial de los montones personalizados, buscar formas de liberar recursos que solo se utilizan temporalmente y volver a cargarlos cuando sea necesario. Ocasionalmente encontrará una estructura de 64 bytes que podría reducirse a 16, o lo que sea, pero rara vez es la fruta que cuelga más abajo. Sin embargo, si sabes cuáles son las listas y matrices más grandes de la aplicación, entonces sabes qué estructuras mirar primero.
Oh sí: busca y arregla fugas de memoria. Cualquier recuerdo que pueda recuperar sin sacrificar el rendimiento es un gran comienzo.
He pasado mucho tiempo en el pasado preocupándome por el tamaño del código. Las consideraciones principales (aparte de: asegúrese de medirlo en el tiempo de compilación para que pueda verlo cambiar), son:
1) Averigüe a qué código se hace referencia, y por qué. Si descubre que una biblioteca XML completa está siendo vinculada a su aplicación solo para analizar un archivo de configuración de dos elementos, considere cambiar el formato del archivo de configuración y / o escribir su propio analizador trivial. Si puede, use un análisis de fuente o binario para dibujar un gran gráfico de dependencia, y busque componentes grandes con solo un pequeño número de usuarios: puede ser posible cortarlos con solo reescrituras de código menores. Prepárate para ser diplomático: si dos componentes diferentes de tu aplicación usan XML y quieres cortarlo, entonces tienes que convencer a dos personas de los beneficios de rodar a mano algo que actualmente es una biblioteca confiable y lista para usar. .
2) Meterse con las opciones del compilador. Consulte la documentación específica de su plataforma. Por ejemplo, es posible que desee reducir el aumento del tamaño del código aceptable por defecto debido a la incorporación, y en GCC al menos puede decirle al compilador que aplique optimizaciones que normalmente no aumentan el tamaño del código.
3) Aproveche las bibliotecas que ya están en la (s) plataforma (s) de destino cuando sea posible, incluso si eso significa escribir una capa de adaptador. En el ejemplo XML anterior, puede encontrar que en su plataforma de destino siempre hay una biblioteca XML en la memoria, porque el SO la usa, en cuyo caso se vincula dinámicamente.
4) Como mencionó otra persona, el modo pulgar puede ayudar en ARM. Si solo lo usa para el código que no es crítico para el rendimiento, y deja las rutinas críticas en ARM, entonces no notará la diferencia.
Finalmente, puede haber trucos inteligentes que pueda jugar si tiene suficiente control sobre el dispositivo. ¿La interfaz de usuario solo permite que una aplicación se ejecute a la vez? Descargue todos los controladores y servicios que su aplicación no necesita. La pantalla tiene doble buffer, pero ¿tu aplicación está sincronizada con el ciclo de actualización? Es posible que pueda reclamar un búfer de pantalla completo.
¿Cuáles son algunas de las mejores prácticas para la "Programación C con memoria eficiente". Sobre todo para dispositivos incrustados / móviles, ¿cuáles deberían ser las pautas para tener consumos de memoria bajos?
Supongo que debería haber una guía separada para a) memoria de código b) memoria de datos
Además de las sugerencias que otros han dado, recuerde que las variables locales declaradas en sus funciones normalmente se asignarán en la pila.
Si la memoria de la pila es limitada o si desea reducir el tamaño de la pila para dejar espacio para más memoria RAM global o global, considere lo siguiente:
- Aplane su árbol de llamadas para reducir el número de variables en la pila en cualquier momento dado.
Convierta las variables locales grandes en globales (disminuye la cantidad de pila utilizada, pero aumenta la cantidad de RAM global utilizada). Las variables se pueden declarar:
- Alcance global: visible para todo el programa
- Estático en el alcance del archivo: visible en el mismo archivo
- Estático en el alcance de la función: visible dentro de la función
- NOTA: Independientemente, si se realizan estos cambios, debe tener cuidado con los problemas con el código de
reentrant
si tiene un entornopreemptive
.
Muchos sistemas incorporados no tienen un diagnóstico de monitor de pila para asegurar que se captura un desbordamiento de pila , por lo que se requiere algún análisis.
PD: ¿Bonificación por el uso apropiado de ?
Algunas operaciones de análisis se pueden realizar en las transmisiones a medida que llegan los bytes en lugar de copiar en el búfer y analizar.
Algunos ejemplos de esto:
- Parsee un NMEA steam con una máquina de estados, recolectando solo los campos requeridos en una estructura mucho más eficiente.
- Parse XML usando SAX en lugar de DOM.
Algunas sugerencias que he encontrado útiles para trabajar con sistemas integrados:
Asegúrese de que cualquier tabla de búsqueda u otros datos constantes se declaren realmente usando
const
. Si se usaconst
, los datos se pueden almacenar en memoria de solo lectura (por ejemplo, flash o EEPROM); de lo contrario, los datos deben copiarse en la RAM al inicio, y esto ocupa tanto espacio en memoria flash como RAM. Establezca las opciones del vinculador para que genere un archivo de mapa y estudie este archivo para ver exactamente dónde están asignados sus datos en el mapa de memoria.Asegúrese de estar utilizando todas las regiones de memoria disponibles para usted. Por ejemplo, los microcontroladores a menudo tienen memoria integrada que puede utilizar (que también puede ser más rápida de acceder que la RAM externa). Debería poder controlar las regiones de memoria a las que se asignan el código y los datos utilizando las configuraciones de opciones de compilador y enlazador.
Para reducir el tamaño del código, verifique la configuración de optimización de su compilador. La mayoría de los compiladores tienen conmutadores para optimizar la velocidad o el tamaño del código. Puede valer la pena experimentar con estas opciones para ver si se puede reducir el tamaño del código compilado. Y obviamente, elimine el código duplicado siempre que sea posible.
Verifique cuánta memoria de pila necesita su sistema y ajuste la asignación de memoria del enlazador en consecuencia (consulte las respuestas a esta pregunta ). Para reducir el uso de la pila, evite colocar estructuras de datos grandes en la pila (para cualquier valor de "grande" es relevante para usted).
Asegúrate de estar usando matemáticas de punto fijo / entero siempre que sea posible. Muchos desarrolladores usan matemática de punto flotante (junto con el bajo rendimiento y las grandes bibliotecas y el uso de memoria) cuando basta una simple escala de enteros matemáticos.
En C, en un nivel mucho más simple, considere lo siguiente;
- Utilice #pragma pack (1) para alinear sus estructuras con byte
- Usa uniones donde una estructura puede contener diferentes tipos de datos
- Use campos de bit en lugar de ints para almacenar indicadores y enteros pequeños
- Evite utilizar arreglos de caracteres de longitud fija para almacenar cadenas, implementar un grupo de cadenas y usar punteros.
- Donde almacenar referencias a una lista de cadenas enumeradas, por ejemplo, nombre de fuente, almacenar un índice en la lista en lugar de la cadena
- Al usar la asignación de memoria dinámica, calcule la cantidad de elementos requeridos por adelantado para evitar las reasignaciones.
Evite la fragmentación de memoria usando su propio asignador de memoria (o usando cuidadosamente el asignador del sistema).
Un método es utilizar un ''asignador de slab'' (consulte este article por ejemplo) y varios grupos de memoria para objetos de diferentes tamaños.
Lo más probable es que deba elegir sus algoritmos cuidadosamente. Apunte a algoritmos que tienen uso de memoria O (1) u O (log n) (es decir, bajo). Por ejemplo, las matrices de tamaño variable continuo (por ejemplo, std::vector
) en la mayoría de los casos requieren menos memoria que las listas vinculadas.
En ocasiones, el uso de tablas de búsqueda puede ser más beneficioso para el tamaño y la velocidad del código. Si solo necesita 64 entradas en una LUT, son 16 * 4 bytes para sin / cos / tan (¡use simetría!) En comparación con una función sin / cos / tangente grande.
La compresión a veces ayuda. Algoritmos simples como RLE son fáciles de comprimir / descomprimir cuando se leen de forma secuencial.
Si se trata de gráficos o audio, considere diferentes formatos. Los gráficos con paleta o con bit * pueden ser una buena compensación para la calidad, y las paletas se pueden compartir en muchas imágenes, reduciendo aún más el tamaño de los datos. El audio puede reducirse de 16 bits a 8 bits o incluso 4 bits, y el estéreo se puede convertir en mono. Las tasas de muestreo pueden reducirse de 44.1KHz a 22kHz o 11kHz. Estas transformaciones de audio reducen en gran medida su tamaño de datos (y, por desgracia, la calidad) y son triviales (excepto el remuestreo, pero para eso es el software de audio =]).
* Supongo que podrías poner esto bajo compresión. Bitpacking para gráficos generalmente se refiere a reducir el número de bits por canal para que cada píxel pueda caber en dos bytes (RGB565 o ARGB155 por ejemplo) o uno (ARGB232 o RGB332) de los tres o cuatro originales (RGB888 o ARGB8888, respectivamente).
Preasignar toda la memoria por adelantado (es decir, no hay llamadas malloc, excepto para la inicialización de inicio) es definitivamente útil para el uso determinista de la memoria. De lo contrario, diferentes arquitecturas proporcionan técnicas para ayudar. Por ejemplo, ciertos procesadores ARM proporcionan un conjunto de instrucciones alternativas (Thumb) que casi reduce a la mitad el tamaño del código al usar instrucciones de 16 bits en lugar de los 32 bits normales. Por supuesto, la velocidad se sacrifica al hacerlo ...
Recomienda este libro Small Memory Software: Patrones para sistemas con memoria limitada
Tengo una presentación de la Embedded Systems Conference disponible sobre este tema. Es desde 2001, pero aún así es muy aplicable. Ver paper .
Además, si puede elegir la arquitectura del dispositivo de destino, usar algo como un ARM moderno con Thumb V2, o un PowerPC con VLE, o MIPS con MIPS16, o seleccionar objetivos compactos conocidos como Infineon TriCore o la familia SH es un muy buena opción. Sin mencionar la familia NEC V850E que es muy compacta. O muévase a un AVR que tenga una excelente compacidad de código (pero es una máquina de 8 bits). ¡Cualquier cosa menos un RISC de 32 bits de longitud fija es una buena opción!
Todas las buenas recomendaciones Aquí hay algunos enfoques de diseño que he encontrado útiles.
- Codificación de bytes
Escriba un intérprete para un conjunto de instrucciones de código de bytes de propósito especial, y escriba la mayor cantidad de programa posible en ese conjunto de instrucciones. Si ciertas operaciones requieren un alto rendimiento, conviértalas en código nativo y llámalas del intérprete.
- Codigo de GENERACION
Si parte de los datos de entrada cambian con poca frecuencia, podría tener un generador de código externo que crea un programa ad-hoc. Eso será más pequeño que un programa más general, además de funcionar más rápido y no tener que asignar almacenamiento para la entrada que rara vez cambia.
- Sé un enemigo de los datos
Esté dispuesto a perder muchos ciclos si le permite almacenar una estructura de datos absolutamente mínima. Por lo general, encontrará que el rendimiento sufre muy poco.
Un truco que es útil en las aplicaciones es crear un fondo de memoria para el día lluvioso. Asigne un solo bloque al inicio que sea lo suficientemente grande como para que sea suficiente para las tareas de limpieza. Si malloc / new falla, libere el fondo del día lluvioso y publique un mensaje para informar al usuario que los recursos son escasos y que deberían guardarse y muy pronto. Esta fue una técnica utilizada en muchas aplicaciones Mac alrededor de 1990.
Una forma excelente de limitar los requisitos de memoria es confiar tanto como sea posible en libc u otras bibliotecas estándar que se puedan vincular dinámicamente. Cada DLL u objeto compartido adicional que debe incluir en su proyecto es una gran cantidad de memoria que puede evitar quemar.
Además, utilice uniones y campos de bits, cuando corresponda, solo cargue en la memoria la parte de los datos en los que su programa está trabajando y asegúrese de que está compilando con el interruptor -Os (en gcc o el equivalente de su compilador) para optimizar para el tamaño del programa.
Reduzca la longitud de y elimine tantas constantes de cadena como sea posible para reducir el espacio de código
Considere cuidadosamente las compensaciones de algoritmos versus tablas de búsqueda donde sea necesario
Tenga en cuenta cómo se asignan los diferentes tipos de variables.
- Las constantes están probablemente en el espacio del código.
- Las variables estáticas probablemente se encuentren en ubicaciones de memoria fija. Evite estos si es posible.
- Los parámetros probablemente se almacenan en la pila o en los registros.
- Las variables locales también se pueden asignar desde la pila. No declare arrays o cadenas locales grandes si el código se puede quedar sin espacio en la pila en las peores condiciones.
- Es posible que no tenga un montón, es posible que no haya un sistema operativo para administrar el montón para usted. Es eso aceptable? ¿Necesita una función malloc ()?