¿Usar malloc () y free () es una muy mala idea en Arduino?


49

El uso de malloc()y free()parece bastante raro en el mundo Arduino. Se usa en AVR C puro con mucha más frecuencia, pero con precaución.

¿Es realmente una mala idea usar malloc()y free()con Arduino?


2
de lo contrario, se quedará sin memoria realmente rápido, y si sabe cuánta memoria usará, también podría asignarla estáticamente de todos modos
Ratchet Freak

1
No sé si es malo , pero creo que no se usa porque casi nunca se queda sin RAM para la mayoría de los bocetos y es solo una pérdida de flash y preciosos ciclos de reloj. Además, no olvide el alcance (aunque no sé si ese espacio todavía está asignado para todas las variables).
Anonymous Penguin

44
Como de costumbre, la respuesta correcta es "depende". No ha proporcionado suficiente información para saber con certeza si la asignación dinámica es adecuada para usted.
WineSoaked

Respuestas:


40

Mi regla general para sistemas embebidos es solo malloc()grandes buffers y solo una vez, al inicio del programa, por ejemplo, en setup(). El problema surge cuando asigna y desasigna memoria. Durante una sesión de ejecución larga, la memoria se fragmenta y, finalmente, una asignación falla debido a la falta de un área libre suficientemente grande, a pesar de que la memoria libre total es más que adecuada para la solicitud.

(Perspectiva histórica, omita si no le interesa): según la implementación del cargador, la única ventaja de la asignación en tiempo de ejecución frente a la asignación en tiempo de compilación (globales inicializados) es el tamaño del archivo hexadecimal. Cuando los sistemas embebidos se construían con computadoras listas para usar que tenían toda la memoria volátil, el programa a menudo se cargaba al sistema embebido desde una red o una computadora de instrumentación y el tiempo de carga a veces era un problema. Dejar buffers llenos de ceros de la imagen podría acortar el tiempo considerablemente).

Si necesito asignación de memoria dinámica en un sistema embebido, generalmente malloc(), o preferiblemente, asigno estáticamente, un grupo grande y lo divido en búferes de tamaño fijo (o un grupo de búferes pequeños y grandes, respectivamente) y hago mi propia asignación / desasignación de ese grupo. Entonces, cada solicitud de cualquier cantidad de memoria hasta el tamaño del búfer fijo se atiende con uno de esos búferes. La función de llamada no necesita saber si es más grande de lo solicitado, y al evitar dividir y volver a combinar bloques, resolvemos la fragmentación. Por supuesto, aún pueden producirse pérdidas de memoria si el programa tiene errores de asignación / desasignación.


Otra nota histórica, esto condujo rápidamente al segmento BSS, que permitió que un programa ponga a cero su propia memoria para la inicialización, sin copiar lentamente los ceros durante la carga del programa.
rsaxvc

16

Normalmente, al escribir bocetos de Arduino, evitará la asignación dinámica (ya sea con malloco newpara instancias de C ++), las personas prefieren utilizar staticvariables globales o variables locales (pila).

El uso de la asignación dinámica puede generar varios problemas:

  • pérdidas de memoria (si pierde un puntero a una memoria que asignó anteriormente, o más probablemente si olvida liberar la memoria asignada cuando ya no la necesita)
  • la fragmentación del montón (después de varios malloc/ freellamadas), donde la pila se hace más grande thant la cantidad real de memoria asignada actualmente

En la mayoría de las situaciones que he enfrentado, la asignación dinámica no era necesaria o podría evitarse con macros como en el siguiente ejemplo de código:

MySketch.ino

#define BUFFER_SIZE 32
#include "Dummy.h"

Dummy.h

class Dummy
{
    byte buffer[BUFFER_SIZE];
    ...
};

Sin esto #define BUFFER_SIZE, si quisiéramos que la Dummyclase tuviera un buffertamaño no fijo , tendríamos que usar la asignación dinámica de la siguiente manera:

class Dummy
{
    const byte* buffer;

    public:
    Dummy(int size):buffer(new byte[size])
    {
    }

    ~Dummy()
    {
        delete [] bufer;
    }
};

En este caso, tenemos más opciones que en la primera muestra (p. Ej., Usar diferentes Dummyobjetos con diferentes buffertamaños para cada uno), pero podemos tener problemas de fragmentación del montón.

Tenga en cuenta el uso de un destructor para garantizar que la memoria asignada dinámicamente bufferse liberará cuando Dummyse elimine una instancia.


14

He echado un vistazo al algoritmo utilizado por malloc()avr-libc, y parece que hay algunos patrones de uso que son seguros desde el punto de vista de la fragmentación del montón:

1. Asigne solo buffers de larga duración

Con esto quiero decir: asigne todo lo que necesita al comienzo del programa, y ​​nunca lo libere. Por supuesto, en este caso, también podría usar buffers estáticos ...

2. Asigne solo buffers de corta duración

Significado: libera el búfer antes de asignar cualquier otra cosa. Un ejemplo razonable podría verse así:

void foo()
{
    size_t size = figure_out_needs();
    char * buffer = malloc(size);
    if (!buffer) fail();
    do_whatever_with(buffer);
    free(buffer);
}

Si no hay malloc en el interior do_whatever_with(), o si esa función libera lo que asigne, entonces está a salvo de la fragmentación.

3. Libere siempre el último búfer asignado

Esta es una generalización de los dos casos anteriores. Si usa el montón como una pila (el último en entrar es el primero en salir), entonces se comportará como una pila y no se fragmentará. Cabe señalar que en este caso es seguro cambiar el tamaño del último búfer asignado con realloc().

4. Siempre asigne el mismo tamaño

Esto no evitará la fragmentación, pero es seguro en el sentido de que el montón no crecerá más que el tamaño máximo utilizado . Si todas sus memorias intermedias tienen el mismo tamaño, puede estar seguro de que, cuando libere una de ellas, la ranura estará disponible para asignaciones posteriores.


1
Se debe evitar el patrón 2 ya que agrega ciclos para malloc () y free () cuando esto se puede hacer con "char buffer [size];" (en C ++). También me gustaría agregar el antipatrón "Nunca de un ISR".
Mikael Patel

9

El uso de la asignación dinámica (a través de malloc/ freeo new/ delete) no es inherentemente malo como tal. De hecho, para algo como el procesamiento de cadenas (por ejemplo, a través del Stringobjeto), a menudo es bastante útil. Esto se debe a que muchos bocetos usan varios fragmentos pequeños de cadenas, que eventualmente se combinan en uno más grande. El uso de la asignación dinámica le permite usar solo la cantidad de memoria que necesita para cada uno. En contraste, usar un buffer estático de tamaño fijo para cada uno podría terminar desperdiciando mucho espacio (haciendo que se quede sin memoria mucho más rápido), aunque depende completamente del contexto.

Dicho todo esto, es muy importante asegurarse de que el uso de la memoria sea predecible. Permitir que el boceto use cantidades arbitrarias de memoria dependiendo de las circunstancias del tiempo de ejecución (por ejemplo, entrada) puede causar un problema fácilmente tarde o temprano. En algunos casos, puede ser perfectamente seguro, por ejemplo, si sabe que el uso nunca sumará demasiado. Sin embargo, los bocetos pueden cambiar durante el proceso de programación. Una suposición hecha desde el principio podría olvidarse cuando algo se cambia más tarde, lo que resulta en un problema imprevisto.

Para mayor robustez, generalmente es mejor trabajar con memorias intermedias de tamaño fijo siempre que sea posible, y diseñar el boceto para que funcione explícitamente con esos límites desde el principio. Eso significa que cualquier cambio futuro en el boceto, o cualquier circunstancia inesperada en el tiempo de ejecución, no debería causar problemas de memoria.


6

No estoy de acuerdo con las personas que piensan que no deberías usarlo o que generalmente es innecesario. Creo que puede ser peligroso si no conoce los entresijos, pero es útil. Tengo casos en los que no sé (y no debería importarme saber) el tamaño de una estructura o un búfer (en tiempo de compilación o tiempo de ejecución), especialmente cuando se trata de bibliotecas que envío al mundo. Estoy de acuerdo en que si su aplicación solo trata con una estructura única y conocida, debe hornear ese tamaño en el momento de la compilación.

Ejemplo: tengo una clase de paquete en serie (una biblioteca) que puede tomar cargas de datos de longitud arbitraria (puede ser struct, array de uint16_t, etc.). Al final del envío de esa clase, simplemente le dice al método Packet.send () la dirección de la cosa que desea enviar y el puerto HardwareSerial a través del cual desea enviarlo. Sin embargo, en el extremo receptor, necesito un búfer de recepción asignado dinámicamente para mantener esa carga útil entrante, ya que esa carga podría ser una estructura diferente en cualquier momento dado, dependiendo del estado de la aplicación, por ejemplo. SI solo envío una sola estructura de un lado a otro, simplemente haría que el búfer tenga el tamaño que necesita en el momento de la compilación. Pero, en el caso de que los paquetes puedan tener diferentes longitudes en el tiempo, malloc () y free () no son tan malos.

He realizado pruebas con el siguiente código durante días, lo que permite que se repita continuamente, y no he encontrado evidencia de fragmentación de la memoria. Después de liberar la memoria asignada dinámicamente, la cantidad libre vuelve a su valor anterior.

// found at learn.adafruit.com/memories-of-an-arduino/measuring-free-memory
int freeRam () {
    extern int __heap_start, *__brkval;
    int v;
    return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval);
}

uint8_t *_tester;

while(1) {
    uint8_t len = random(1, 1000);
    Serial.println("-------------------------------------");
    Serial.println("len is " + String(len, DEC));
    Serial.println("RAM: " + String(freeRam(), DEC));
    Serial.println("_tester = " + String((uint16_t)_tester, DEC));
    Serial.println("alloating _tester memory");
    _tester = (uint8_t *)malloc(len);
    Serial.println("RAM: " + String(freeRam(), DEC));
    Serial.println("_tester = " + String((uint16_t)_tester, DEC));
    Serial.println("Filling _tester");
    for (uint8_t i = 0; i < len; i++) {
        _tester[i] = 255;
    }
    Serial.println("RAM: " + String(freeRam(), DEC));
    Serial.println("freeing _tester memory");
    free(_tester); _tester = NULL;
    Serial.println("RAM: " + String(freeRam(), DEC));
    Serial.println("_tester = " + String((uint16_t)_tester, DEC));
    delay(1000); // quick look
}

No he visto ningún tipo de degradación en la RAM o en mi capacidad de asignarla dinámicamente usando este método, por lo que diría que es una herramienta viable. FWIW


2
Su código de prueba se ajusta al patrón de uso 2. Asigne solo buffers de corta duración que describí en mi respuesta anterior. Este es uno de esos pocos patrones de uso que se sabe que son seguros.
Edgar Bonet

En otras palabras, los problemas surgirán cuando comience a compartir el procesador con otro código desconocido , que es precisamente el problema que cree que está evitando. En general, si desea algo que siempre funcionará o fallará durante la vinculación, realiza una asignación fija del tamaño máximo y lo usa una y otra vez, por ejemplo, haciendo que su usuario se lo pase en la inicialización. Recuerde que normalmente se está ejecutando en un chip donde todo tiene que caber en 2048 bytes, tal vez más en algunas placas pero también mucho menos en otras.
Chris Stratton

@EdgarBonet Sí, exactamente. Solo quería compartir.
StuffAndyMakes

1
Asignar dinámicamente un búfer de solo el tamaño necesario es arriesgado, ya que si se asigna algo más antes de liberarlo, puede quedarse con fragmentación, memoria que no puede reutilizar. Además, la asignación dinámica tiene gastos generales de seguimiento. La asignación fija no significa que no puede multiplicar el uso de la memoria, solo significa que debe trabajar para compartir el diseño de su programa. Para un búfer con alcance puramente local, también puede sopesar el uso de la pila. Tampoco ha verificado la posibilidad de que malloc () falle.
Chris Stratton

1
"Puede ser peligroso si no conoce los entresijos, pero es útil". prácticamente resume todo el desarrollo en C / C ++. :-)
ThatAintWorking

4

¿Es realmente una mala idea usar malloc () y free () con Arduino?

La respuesta corta es sí. A continuación se detallan los motivos:

Se trata de comprender qué es una MPU y cómo programar dentro de las limitaciones de los recursos disponibles. El Arduino Uno utiliza una MPU ATmega328p con memoria flash ISP de 32KB, EEPROM 1024B y SRAM de 2KB. Eso no es una gran cantidad de recursos de memoria.

Recuerde que la SRAM de 2 KB se usa para todas las variables globales, literales de cadena, pila y posible uso del montón. La pila también necesita tener espacio para un ISR.

El diseño de la memoria es:

Mapa SRAM

Las PC / laptops actuales tienen más de 1,000,000 de veces la cantidad de memoria. Un espacio de pila predeterminado de 1 Mbyte por subproceso no es infrecuente pero totalmente irreal en una MPU.

Un proyecto de software integrado tiene que hacer un presupuesto de recursos. Esto es estimar la latencia ISR, el espacio de memoria necesario, la potencia de cómputo, los ciclos de instrucción, etc. Desafortunadamente no hay almuerzos gratis y la programación incrustada en tiempo real es la más difícil de las habilidades de programación para dominar.


Amén a eso: "[H] ard la programación incrustada en tiempo real es la más difícil de las habilidades de programación para dominar".
StuffAndyMakes

¿El tiempo de ejecución de malloc es siempre el mismo? ¿Puedo imaginar que Malloc tome más tiempo mientras busca más en la memoria RAM disponible una ranura que se ajuste? ¿Este sería otro argumento (aparte de quedarse sin memoria RAM) para no asignar memoria sobre la marcha?
Paul

@Paul Los algoritmos de almacenamiento dinámico (malloc y gratuito) generalmente no tienen un tiempo de ejecución constante y no son reentrantes. El algoritmo contiene estructuras de búsqueda y datos que requieren bloqueos cuando se usan hilos (concurrencia).
Mikael Patel

0

Ok, sé que esta es una vieja pregunta, pero cuanto más leo las respuestas, más vuelvo a una observación que parece sobresaliente.

El problema de detención es real

Parece que hay un vínculo con el problema de detención de Turing aquí. Permitir la asignación dinámica aumenta las probabilidades de dicha 'detención', por lo que la pregunta se convierte en una de tolerancia al riesgo. Si bien es conveniente descartar la posibilidad de malloc()fallar, etc., sigue siendo un resultado válido. La pregunta que hace el OP solo parece ser sobre la técnica, y sí, los detalles de las bibliotecas utilizadas o la MPU específica son importantes; la conversación gira hacia la reducción del riesgo de que el programa se detenga o cualquier otro final anormal. Necesitamos reconocer la existencia de entornos que toleran el riesgo de manera muy diferente. Mi proyecto de pasatiempo para mostrar colores bonitos en una tira de LED no matará a alguien si sucede algo inusual, pero la MCU dentro de una máquina de corazón y pulmón probablemente lo hará.

Hola señor Turing, mi nombre es Hubris

Para mi tira de LED, no me importa si se bloquea, lo restableceré. Si estuviese en una máquina corazón-pulmón controlada por un MCU, las consecuencias de que se bloqueara o no funcionara son literalmente la vida y la muerte, por lo que la pregunta sobre malloc()y cómo free()debería dividirse entre cómo el programa previsto trata con la posibilidad de demostrarle al Sr. El famoso problema de Turing. Puede ser fácil olvidar que es una prueba matemática y convencernos de que si solo somos lo suficientemente inteligentes podemos evitar ser víctimas de los límites de la computación.

Esta pregunta debe tener dos respuestas aceptadas, una para aquellos que se ven obligados a parpadear al mirar El problema detenido en la cara, y otra para todos los demás. Si bien la mayoría de los usos del arduino probablemente no sean aplicaciones de misión crítica o de vida o muerte, la distinción sigue existiendo independientemente de qué MPU esté codificando.


No creo que el problema de detención se aplique en esta situación específica teniendo en cuenta el hecho de que el uso del montón no es necesariamente arbitrario. Si se usa de manera bien definida, el uso del montón se vuelve previsiblemente "seguro". El punto del problema de detención fue determinar si se puede determinar qué sucede con un algoritmo necesariamente arbitrario y no tan bien definido. Realmente se aplica mucho más a la programación en un sentido más amplio y, como tal, considero que aquí específicamente no es muy relevante. Ni siquiera creo que sea relevante en absoluto para ser completamente honesto.
Jonathan Gray

Admitiré alguna exageración retórica, pero el punto es realmente si quieres garantizar el comportamiento, usar el montón implica un nivel de riesgo que es mucho más alto que limitarse a usar solo la pila.
Kelly S. French

-3

No, pero deben usarse con mucho cuidado en lo que respecta a liberar () la memoria asignada. Nunca he entendido por qué la gente dice que se debe evitar la gestión directa de la memoria, ya que implica un nivel de incompetencia que generalmente es incompatible con el desarrollo de software.

Digamos que estás usando tu arduino para controlar un dron. Cualquier error en cualquier parte de su código podría causar que se caiga del cielo y lastime a alguien o algo. En otras palabras, si alguien carece de las competencias para usar malloc, es probable que no deba codificar en absoluto, ya que hay muchas otras áreas donde los pequeños errores pueden causar problemas graves.

¿Los errores causados ​​por malloc son más difíciles de rastrear y corregir? Sí, pero eso es más una cuestión de frustración por parte de los codificadores que de riesgo. En lo que respecta al riesgo, cualquier parte de su código puede ser igual o más arriesgado que malloc si no sigue los pasos para asegurarse de que se haga correctamente.


44
Es interesante que hayas usado un dron como ejemplo. De acuerdo con este artículo ( mil-embedded.com/articles/… ), "Debido a su riesgo, la asignación dinámica de memoria está prohibida, bajo el estándar DO-178B, en el código de aviónica incrustado de seguridad crítica".
Gabriel Staples

DARPA tiene una larga historia de permitir que los contratistas desarrollen especificaciones que se ajusten a su propia plataforma, ¿por qué no deberían hacerlo cuando son los contribuyentes los que pagan la factura? Es por eso que les cuesta $ 10 mil millones desarrollar lo que otros pueden hacer con $ 10,000. Casi suena como si estuvieras usando el complejo industrial militar como una referencia honesta.
JSON

La asignación dinámica parece una invitación para que su programa demuestre los límites de cálculo descritos en el Problema de detención. Hay algunos entornos que pueden manejar una pequeña cantidad de riesgo de tal detención y existen entornos (espacio, defensa, médicos, etc.) que no tolerarán ninguna cantidad de riesgo controlable, por lo que no permiten operaciones que "no deberían" falla porque "debería funcionar" no es lo suficientemente bueno cuando está lanzando un cohete o controlando una máquina de corazón / pulmón.
Kelly S. French