¿Cómo preservar el espacio de la pila con un buen diseño?
memory embedded (10)
Estoy programando en C para el microcontrolador embebido RAM limitado con RTOS.
Regularmente rompo mi código en funciones cortas, pero cada llamada de función requiere más memoria de la pila. Cada tarea necesita su pila, y este es uno de los consumidores de memoria más importantes en el proyecto.
¿Hay alguna alternativa para mantener el código bien organizado y legible, aún así conservar la memoria?
¿Puedes reemplazar algunas de tus variables locales por globales? Las matrices en particular pueden devorar la pila.
Si la situación le permite compartir algunos valores globales entre algunos entre las funciones, existe la posibilidad de que pueda reducir la huella de memoria.
El costo de compensación es una mayor complejidad y un mayor riesgo de efectos secundarios no deseados entre funciones frente a una posible pérdida de memoria.
¿Qué tipo de variables tienes en tus funciones? ¿De qué tamaños y límites estamos hablando?
Active la optimización, específicamente la creación de líneas agresivas. El compilador debería poder alinear métodos para minimizar llamadas. Según el compilador y los conmutadores de optimización que utilice, marcar algunos métodos como en inline
puede ayudar (o puede ignorarse).
Con GCC, intente agregar el indicador "-finline-functions" (u -O3) y posiblemente el indicador "-finline-limit = n".
Creo que puedes estar imaginando un problema que no existe aquí. La mayoría de los compiladores en realidad no hacen nada cuando "asignan" variables automáticas en la pila.
La pila se asigna antes de que se ejecute "main ()". Cuando llama a la función b () desde la función a () la dirección del área de almacenamiento inmediatamente después de pasar la última variable utilizada por a a b (). Esto se convierte en el inicio de la pila de b () si b () luego llama a la función c (), entonces la pila de c comienza después de la última variable automática definida por b ().
Tenga en cuenta que la memoria de la pila ya está allí y asignada, que no tiene lugar ninguna inicialización y que el único procesamiento involucrado es pasar un puntero de la pila.
El único momento en que esto se convierte en un problema sería cuando las tres funciones usan grandes cantidades de almacenamiento y la pila debe acomodarse a la memoria de las tres funciones. Trate de mantener las funciones que asignan grandes cantidades de almacenamiento en la parte inferior de la pila de llamadas, es decir, no llame a otra función desde ellos.
Otro truco para los sistemas constantes de memoria es dividir las partes de memoria que acaparan una función en funciones autónomas separadas.
Dependiendo de su compilador, y cuán agresivas sean sus opciones de optimización, tendrá uso de pila para cada llamada de función que haga. Así que, para empezar, probablemente deba limitar la profundidad de sus llamadas a funciones. Algunos compiladores usan saltos en lugar de ramas para funciones simples que reducirán el uso de la pila. Obviamente, puede hacer lo mismo utilizando, por ejemplo, una macro de ensamblador para saltar a sus funciones en lugar de una llamada de función directa.
Como se mencionó en otras respuestas, la opción de alineación es una opción disponible, aunque eso tiene el costo de un mayor tamaño del código.
La otra área que come pila son los parámetros locales. Esta área sobre la que tienes cierto control. El uso de estática (nivel de archivo) evitará la asignación de la pila a costa de la asignación estática de ram. Globales también.
En casos (verdaderamente) extremos, puede proponer una convención para funciones que utiliza un número fijo de variables globales como almacenamiento temporal en lugar de locales en la pila. Lo difícil es asegurarse de que ninguna de las funciones que utilizan los mismos valores globales se llame al mismo tiempo. (de ahí la convención)
En caso de que pueda ahorrar una gran cantidad de memoria principal pero tenga solo una pequeña porción de pila, le sugiero que evalúe las asignaciones estáticas.
En C, todas las variables declaradas dentro de una función se "administran automáticamente", lo que significa que están asignadas en la pila.
La calificación de las declaraciones como "estáticas" las almacena en la memoria principal en lugar de en la pila. Básicamente se comportan como variables globales, pero aún le permiten evitar los malos hábitos que vienen con el uso excesivo de productos globales. Puede justificar la declaración de búferes / variables grandes y de larga vida como estáticos para reducir la presión en la pila.
Tenga en cuenta que esto no funciona bien / nada si su aplicación es multiproceso o si usa recursividad.
Hay 3 componentes para tu uso de pila:
- Funciones de devolución de llamadas
- Parámetros de llamada de función
- variables automáticas (locales)
La clave para minimizar el uso de su pila es minimizar el paso de parámetros y las variables automáticas. El consumo de espacio de la llamada a la función real en sí es bastante mínimo.
Parámetros
Una forma de abordar el problema de los parámetros es pasar una estructura (a través de un puntero) en lugar de una gran cantidad de parámetros.
foo(int a, int b, int c, int d)
{
...
bar(int a, int b);
}
haz esto en su lugar:
struct my_params {
int a;
int b;
int c;
int d;
};
foo(struct my_params* p)
{
...
bar(p);
};
Esta estrategia es buena si transfieres muchos parámetros. Si los parámetros son todos diferentes, entonces podría no funcionarle bien. Usted terminaría con una estructura grande que se pasa y que contiene muchos parámetros diferentes.
Variables automáticas (locales)
Este tiende a ser el mayor consumidor de espacio de pila.
- Las matrices son el asesino. ¡No defina arrays en sus funciones locales!
- Minimice el número de variables locales.
- Use el tipo más pequeño que sea necesario.
- Si la reincorporación no es un problema, puede usar variables estáticas del módulo.
Tenga en cuenta que si simplemente está moviendo todas sus variables locales del ámbito local al alcance del módulo, NO ha guardado ningún espacio. Has cambiado el espacio de la pila por el espacio del segmento de datos.
Algunos RTOS admiten el almacenamiento local de subprocesos, que asigna almacenamiento "global" por subproceso. Esto podría permitirle tener múltiples variables globales independientes por tarea, pero esto hará que su código no sea tan directo.
Sí, un RTOS realmente puede consumir RAM para el uso de la pila de tareas. Mi experiencia es que como nuevo usuario de un RTOS, hay una tendencia a usar más tareas de las necesarias.
Para un sistema integrado que usa un RTOS, la RAM puede ser un bien valioso. Para preservar la memoria RAM, para funciones simples, puede ser efectivo implementar varias funciones dentro de una misma tarea, ejecutarse en forma conjunta, con un diseño cooperativo multitarea. Por lo tanto, reduzca el número total de tareas.
Si necesita comenzar a preservar el espacio de pila, debería obtener un compilador mejor o más memoria.
Su software generalmente crecerá (nuevas funciones, ...), por lo que si tiene que comenzar un proyecto pensando en cómo conservar el espacio de la pila, está condenado desde el principio.
Un truco que leí en algún lugar para evaluar los requisitos de pila del código en una configuración incrustada es llenar el espacio de la pila al inicio con un patrón conocido (DEAD en hexadecimal es mi favorito) y dejar que el sistema funcione por un tiempo.
Después de una ejecución normal, lea el espacio de la pila y vea cuánto del espacio de la pila no se ha reemplazado durante el curso de la operación. Diseñe para dejar al menos el 150% de eso para abordar todos los caminos obscuros del código que pudieron no haberse ejercitado.
Intenta hacer que la pila de llamadas sea más plana, así que en lugar de a()
llamar a b()
que llama a c()
que llama a d()
, tiene a()
llamada b()
, c()
y d()
.
Si una función solo se referencia una vez, márquela en inline
(suponiendo que su compilador lo admita).