¿Realloc desperdiciando mucho espacio en mi MCU?

Estoy escribiendo un programador de tareas simple y usando la asignación de memoria dinámica en mi cc430F5137. Estoy de acuerdo en que no es una buena práctica, pero por el momento supongamos que es un requisito de mi aplicación usar la asignación de memoria dinámica.

En mi archivo OS.c

tengo dos estructuras

typedef struct
{
    task_t task;
    uint8_t next;
    uint8_t prev;
    uint8_t priority;

} info_t;


typedef struct
{
    task_t task;
    uint8_t index;
} index_t;

el tamaño de info_tes de 8 bytes y el tamaño de index_tes de 6 bytes.

también lo hago

index_t* m_index;
info_t* m_info;

Entonces tengo la función de inicialización en la que hago

m_info = NULL;
m_index = NULL;

Ahora tengo una función registerTask(& task)que toma la dirección de la función para programar. En esta función hago

m_info = realloc(m_info,(num_registered_tasks + 1) * sizeof(*m_info));

y luego establezca los valores de .priority, next,task y prev.

Entonces lo hago

m_index = realloc(m_index,(num_registered_tasks + 1) * sizeof(*m_index));

y establezca los valores de task e index. y hacernum_registered_tasks++;

Mi pregunta es que como se está realloc()comportando en este sentido.

Supongamos que mi espacio de memoria, First Task está registrado, por lo que tendrá los primeros 8 bytes para m_info[0]y los siguientes 6 bytes para m_index[0]. Ahora, cuando mi segunda tarea llame a esta función, ¿qué sucederá? Lo que supongo es que para m_info primero buscará 16 bytes de datos continuos y solo los encontrará después de los primeros 14 bytes, cambiará la dirección m_info[0]y copiará el contenido y luego agregará m_info[1]. Y cuando m_indexse llama, solo encontrará 12 bytes después de este (14 + 16) bytes y lugar m_index[0]y m_index[1]aquí.

Si esto es cierto, ¿se está desperdiciando todo mi espacio anterior?

Si me equivoco, ¿cómo funcionará esta función?

¿Cómo puedo utilizar el espacio anterior también?

Necesito index_tuna estructura para implementar algún tipo de algoritmo de búsqueda, por lo que también es necesario

Esto puede ser muy específico de la implementación. Sí, en general, puede esperar que la primera reasignación tenga que encontrar un bloque contiguo "en algún lugar" y que probablemente estará más allá del segundo bloque. El espacio ocupado por el primer bloque original técnicamente quedará libre, pero lo que suceda depende mucho de la implementación. Tal vez se reutilice solo (si es lo suficientemente grande) más tarde, tal vez se fusione con el segundo bloque cuando este último se libere y se reutilicen juntos.
¿Por qué necesita que todas sus estructuras info_t estén ubicadas juntas en un bloque de memoria contiguo? Tiene punteros de siguiente y anterior, por lo que no hay nada que le impida asignar bloques individuales para estructuras info_t individuales y usar esos punteros de siguiente y anterior para mantener su lista vinculada.
Dado que el número de cada uno es siempre el mismo, ¿hay alguna razón por la que sean dos estructuras separadas? Con una MCU sin caché, la localidad de referencia generalmente es menos importante (puede ayudar a simplificar el direccionamiento). Esto eliminaría la fragmentación de estas estructuras y un asignador de memoria personalizado podría limitar la fragmentación de otras asignaciones (por ejemplo, colocar otros objetos en la parte superior del montón/región).

Respuestas (3)

El uso de la asignación de memoria dinámica en una MCU de 16 bits con 4 kb de RAM es una ingeniería muy pobre.

No tanto por los habituales problemas de fugas de memoria. No tanto por la fragmentación de la memoria del montón. Ni siquiera por el elevado tiempo de ejecución necesario para las rutinas de asignación. Sino porque es completamente inútil y no tiene sentido.

Tiene algunos requisitos del mundo real que indican lo que debe hacer su programa y, según estos, su programa necesitará usar exactamente x bytes de RAM para manejar el peor de los casos . No necesitará menos, no necesitará más, necesitará la cantidad exacta de RAM, que se puede determinar en el momento de la compilación.

No tiene sentido guardar parte de los 4kb, dejándolos sin usar. ¿Quién los va a usar? Del mismo modo, no tiene sentido asignar más memoria de la necesaria para el peor de los casos. Simplemente asigne estáticamente tanta memoria como sea necesario, punto.

Además, tiene el peor de los casos con el uso máximo de la pila, en el que se encuentra en lo más profundo de una pila de llamadas y todas las interrupciones que están habilitadas se han disparado. Este peor de los casos puede calcularse en tiempo de compilación o medirse en tiempo de ejecución. Una buena práctica es asignar más RAM de la que se necesita en el peor de los casos, para evitar desbordamientos de la pila (la pila también es en realidad un área de memoria dinámica). O más bien: use cada byte de RAM no utilizado por su programa para la pila.

Si su aplicación necesita exactamente 3 kb de RAM, debe usar el 1 kb restante para la pila.

La asignación dinámica/el montón de la computadora está diseñado para usarse en aplicaciones alojadas como Windows o Linux, donde cada proceso tiene una cantidad fija de RAM y, además, puede usar la memoria del montón para usar tanta RAM como haya en el hardware. Solo tiene sentido utilizar la asignación dinámica en sistemas multiproceso tan complejos, con grandes cantidades de RAM disponibles.

En un programa MCU, ya sea bare metal o basado en RTOS, debe darse cuenta de que las implementaciones de montón funcionan así: reserve una cantidad fija, x kb de RAM para el montón. Cada vez que usa la asignación dinámica, se le entrega una parte de esta cantidad fija de memoria. Entonces, ¿por qué no simplemente tomar esos x kb que usaría para asignar el montón y usarlos para almacenar sus variables?


He ampliado esto con una explicación más detallada y más problemas posibles, aquí:
¿Por qué no debería usar la asignación de memoria dinámica en sistemas integrados?

Entiendo el punto Gracias. Necesitaba una asignación de memoria dinámica para algunas evaluaciones comparativas del programa. No es que esté haciendo alguna aplicación comercial. Encontré una buena solución para esto y mencionaré lo que hice en una respuesta.
El uso de memoria en el peor de los casos no significa necesariamente que una asignación estática solo use esa cantidad de memoria. Además, en algunos diseños, la funcionalidad se puede eliminar con un uso elevado de la memoria, lo que permite eliminar algunas variables activas y liberar su memoria (lo mismo se aplica a otros recursos como el tiempo de procesamiento); con una demanda de memoria variable por funciones, la asignación dinámica puede permitir que el dispositivo conserve una función agradable pero innecesaria en una gama más amplia de condiciones. Incluso reutilizar una parte de la memoria (por ejemplo, desde el búfer de E/S hasta la colección de estructuras) es una forma limitada de memoria dinámica.
@Hassan El uso de la asignación dinámica para la evaluación comparativa tiene aún menos sentido, ya que la introducción de la asignación dinámica probablemente afectará drásticamente el rendimiento. La evaluación comparativa de los sistemas integrados se realiza con un osciloscopio y un pin de E/S de propósito general. Obtendrá cifras comparativas extremadamente precisas de esa manera.
@ PaulA.Clayton "la funcionalidad se puede eliminar con un uso elevado de la memoria, lo que permite eliminar algunas variables en vivo y liberar su memoria" ¿Con qué propósito? ¿Para qué vas a usar esa memoria? O su programa asigna suficiente memoria para manejar el peor de los casos, o no lo hace. Cuando no se ejecuta el peor de los casos, no hay nada significativo que pueda hacer con la llamada memoria "guardada". ¿Está planeando que el programa use esa memoria para hacer algo completamente ajeno a su tarea designada, cuando no se ejecuta en el peor de los casos, o qué?
Como escribí, la funcionalidad que se elimina en "agradable pero innecesaria", la memoria liberada se usa cuando la funcionalidad necesaria se acerca al peor de los casos. Sin la asignación dinámica, es posible que se necesite un dispositivo más costoso (lo que permitiría que la buena funcionalidad esté disponible el 100 % del tiempo en lugar del 99 % del tiempo). Sospecho que la diferencia entre el uso estático (por estructura) de la memoria en el peor de los casos y el uso dinámico de la memoria en el peor de los casos es una consideración más importante, pero la capacidad de admitir la disponibilidad típica de funcionalidad valiosa y no crítica parecía digna de mención.
Como un ejemplo completamente inventado, suponga que uno tiene un registro de eventos que puede contener cinco eventos sin un uso optimista de la memoria, pero con un uso optimista de la memoria, el 90 % del tiempo puede contener 15 eventos, el 9 % del tiempo puede contener 9 eventos y El 1% del tiempo solo podía albergar cuatro eventos. Ampliar la capacidad de registro en el caso típico podría valer la pena el esfuerzo de desarrollo y sacrificar una entrada de evento el 1 % del tiempo. (No es un gran ejemplo porque la profundidad del registro de eventos probablemente sería más importante bajo un alto estrés, pero la prohibición de la memoria dinámica elimina la elección por completo).

Esto se llama fragmentación de la memoria.

Hay todo tipo de algoritmos sofisticados para manejarlo, pero la sobrecarga de contabilidad los hace inadecuados para una MCU pequeña.

En su lugar, puede escribir sus propias rutinas de asignación de memoria en función de su patrón de uso esperado. Puede tener una rutina separada para cada subproceso de ejecución, por ejemplo. Si puede garantizar que free() ocurra en el orden opuesto a alloc(), entonces puede usar un puntero simple al final de la memoria.

O podría usar un nivel de direccionamiento indirecto: acceda a la memoria solo a través de un puntero a un puntero. Luego, puede recolectar basura libremente en la memoria y ajustar la tabla de punteros para que apunte a la memoria donde sea que termine.

La mejor respuesta para los sistemas integrados suele ser evitar por completo la asignación de memoria dinámica.

Hacer uso de patrones de uso es una solución. Otro es usar una matriz de tamaño fijo y no aceptar más objetos de "tarea" que posiblemente puedan caber allí. Sin reasignación, sin preocupaciones.
hola gracias me llega esto. Mis tareas pueden liberarse de forma aleatoria, por lo que no puedo tener este método. Tenía curiosidad por saber si es posible que pueda abordar el espacio de memoria disponible y asignar m_info desde el principio y m_index desde algún lugar cerca del final. m_info y m_index siempre serán iguales en número de elementos
@diente filoso. Estoy de acuerdo, pero no puedo usar esto. También necesito una asignación de memoria dinámica para algunos puntos de referencia.
@Hassan Bien, otra opción. Establece un límite lo suficientemente grande para la cantidad de tareas que puede tener y luego obtiene un bloque grande y lo divide en pequeños fragmentos de modo que cada fragmento pueda almacenar una sola tarea y también un valor booleano que indica si hay una tarea allí. Entonces, cuando necesita almacenar una nueva tarea, simplemente encuentra un fragmento vacío y lo coloca allí.
@sharptooth: ¿no es este método realmente otra forma de describir una matriz ...? :)
@brhans - ¿No es una matriz solo otra forma de describir la memoria principal ...? :)
@brhans Sí, es una matriz pero "faltan" algunos elementos.
No tiene ningún sentido utilizar la asignación de memoria dinámica, entonces, ¿por qué recomendarlo? No quiero ser grosero, pero si usa memoria dinámica para una MCU de 16 bits con 4 kb de RAM, no está usando el sentido común y no está muy seguro de lo que está haciendo, tan simple como eso.

Después de muchos comentarios y respuestas de no usar memoria dinámica, todavía encontré una manera que en ese momento resolvió mi problema.

Cambié esto en mi código

typedef struct
{
    task_t task;
    uint8_t next;
    uint8_t prev;
    uint8_t priority;

} info_t;


typedef struct
{
    task_t task;
    uint8_t index;
} index_t;

typedef struct
{
    info_t m_info;
    info_t m_index;
} combined_t;

combined_t* m_combined;

Con esto, ahora mi realloc() asigna memoria contigua a m_combined y se resuelve mi problema de fragmentación. (Bueno, supongo que porque después de analizar la memoria paso a paso).

Typedefs para definir alias para estructuras es un mal diseño. Utilice estructuras explícitas en su lugar.
@Martin No estoy seguro de entender bien tu comentario. ¿Estás diciendo que typedef struct { int a; } foo_t; foo_t bar;es un mal diseño, mientras que struct foo_s { int a; }; struct foo_s bar;es un buen diseño? Si es así, ¿por qué?