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?
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?
Respuestas:
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.
Normalmente, al escribir bocetos de Arduino, evitará la asignación dinámica (ya sea con malloc
o new
para instancias de C ++), las personas prefieren utilizar static
variables globales o variables locales (pila).
El uso de la asignación dinámica puede generar varios problemas:
malloc
/ free
llamadas), donde la pila se hace más grande thant la cantidad real de memoria asignada actualmenteEn 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 Dummy
clase tuviera un buffer
tamañ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 Dummy
objetos con diferentes buffer
tamañ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 buffer
se liberará cuando Dummy
se elimine una instancia.
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:
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 ...
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.
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()
.
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.
El uso de la asignación dinámica (a través de malloc
/ free
o new
/ delete
) no es inherentemente malo como tal. De hecho, para algo como el procesamiento de cadenas (por ejemplo, a través del String
objeto), 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.
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
¿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:
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.
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.
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á.
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, 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.