L'uso di malloc()
e free()
sembra piuttosto raro nel mondo di Arduino. È usato in puro AVR C molto più spesso, ma ancora con cautela.
È una pessima idea da usare malloc()
e free()
con Arduino?
L'uso di malloc()
e free()
sembra piuttosto raro nel mondo di Arduino. È usato in puro AVR C molto più spesso, ma ancora con cautela.
È una pessima idea da usare malloc()
e free()
con Arduino?
Risposte:
La mia regola generale per i sistemi embedded è solo per i malloc()
buffer di grandi dimensioni e solo una volta, all'inizio del programma, ad es setup()
. Il problema si presenta quando si alloca e si disalloca la memoria. In una sessione di lunga durata, la memoria si frammenta e alla fine un'allocazione non riesce a causa della mancanza di un'area libera sufficientemente ampia, anche se la memoria libera totale è più che adeguata per la richiesta.
(Prospettiva storica, salta se non interessato): a seconda dell'implementazione del caricatore, l'unico vantaggio dell'allocazione in fase di esecuzione rispetto all'allocazione in fase di compilazione (globi inizializzati) è la dimensione del file esadecimale. Quando i sistemi incorporati venivano costruiti con computer pronti all'uso con tutta la memoria volatile, il programma veniva spesso caricato sul sistema incorporato da una rete o da un computer di strumentazione e il tempo di caricamento era talvolta un problema. Tralasciando i buffer pieni di zeri dall'immagine si potrebbe ridurre notevolmente il tempo.)
Se ho bisogno di allocazione dinamica della memoria in un sistema incorporato, in genere malloc()
, o preferibilmente, alloco staticamente un grande pool e lo divido in buffer di dimensioni fisse (o un pool ciascuno di buffer piccoli e grandi, rispettivamente) e faccio la mia allocazione / disallocazione da quel pool. Quindi ogni richiesta per qualsiasi quantità di memoria fino alla dimensione del buffer fisso viene soddisfatta con uno di quei buffer. La funzione di chiamata non ha bisogno di sapere se è più grande di quella richiesta e, evitando la divisione e la ricombinazione dei blocchi, risolviamo la frammentazione. Ovviamente possono verificarsi perdite di memoria se il programma ha allocato / de-allocare bug.
In genere, quando si scrivono schizzi di Arduino, si eviterà l'allocazione dinamica (che si tratti di malloc
o new
per istanze C ++), le persone preferiscono invece utilizzare le static
variabili globali o variabili locali (stack).
L'uso dell'allocazione dinamica può comportare diversi problemi:
malloc
/ free
chiamate) in cui l'heap diventa più grande della quantità effettiva di memoria allocata attualmenteNella maggior parte delle situazioni che ho affrontato, l'allocazione dinamica non era necessaria o poteva essere evitata con le macro come nell'esempio di codice seguente:
MySketch.ino
#define BUFFER_SIZE 32
#include "Dummy.h"
Dummy.h
class Dummy
{
byte buffer[BUFFER_SIZE];
...
};
Senza #define BUFFER_SIZE
, se volessimo che la Dummy
classe abbia una buffer
dimensione non fissa , dovremmo usare l'allocazione dinamica come segue:
class Dummy
{
const byte* buffer;
public:
Dummy(int size):buffer(new byte[size])
{
}
~Dummy()
{
delete [] bufer;
}
};
In questo caso, abbiamo più opzioni rispetto al primo esempio (ad esempio, utilizzare Dummy
oggetti diversi con buffer
dimensioni diverse per ciascuno), ma potremmo avere problemi di frammentazione dell'heap.
Si noti che l'uso di un distruttore per garantire che la memoria allocata in modo dinamico buffer
venga liberata quando Dummy
un'istanza viene eliminata.
Ho dato un'occhiata all'algoritmo usato da malloc()
, da avr-libc, e sembra che ci siano alcuni schemi di utilizzo che sono sicuri dal punto di vista della frammentazione dell'heap:
Con questo intendo: allocare tutto il necessario all'inizio del programma e non liberarlo mai. Naturalmente, in questo caso, potresti anche usare buffer statici ...
Significato: liberate il buffer prima di allocare qualcos'altro. Un esempio ragionevole potrebbe apparire così:
void foo()
{
size_t size = figure_out_needs();
char * buffer = malloc(size);
if (!buffer) fail();
do_whatever_with(buffer);
free(buffer);
}
Se non c'è malloc all'interno do_whatever_with()
o se quella funzione libera qualsiasi cosa alloca, allora sei al sicuro dalla frammentazione.
Questa è una generalizzazione dei due casi precedenti. Se usi l'heap come uno stack (l'ultimo è il primo a uscire), allora si comporterà come uno stack e non come frammento. Va notato che in questo caso è sicuro ridimensionare l'ultimo buffer allocato con realloc()
.
Ciò non impedirà la frammentazione, ma è sicuro nel senso che l'heap non crescerà più delle dimensioni massime utilizzate . Se tutti i buffer hanno le stesse dimensioni, si può essere certi che, ogni volta che si libera uno di essi, lo slot sarà disponibile per le allocazioni successive.
L'uso dell'allocazione dinamica (tramite malloc
/ free
o new
/ delete
) non è intrinsecamente negativo in quanto tale. In effetti, per qualcosa come l'elaborazione delle stringhe (ad esempio tramite l' String
oggetto), è spesso abbastanza utile. Questo perché molti schizzi usano diversi piccoli frammenti di stringhe, che alla fine vengono combinati in uno più grande. L'uso dell'allocazione dinamica consente di utilizzare solo la memoria necessaria per ciascuno di essi. Al contrario, l'uso di un buffer statico di dimensioni fisse per ciascuno potrebbe finire per sprecare molto spazio (causando la sua esaurimento della memoria molto più veloce), anche se dipende interamente dal contesto.
Detto questo, è molto importante assicurarsi che l'utilizzo della memoria sia prevedibile. Consentire allo schizzo di utilizzare quantità arbitrarie di memoria a seconda delle circostanze di runtime (ad es. Input) può facilmente causare un problema prima o poi. In alcuni casi, potrebbe essere perfettamente sicuro, ad esempio se si sa che l'utilizzo non si sommerà mai molto. Tuttavia, gli schizzi possono cambiare durante il processo di programmazione. Un'ipotesi fatta all'inizio potrebbe essere dimenticata quando qualcosa viene cambiato in seguito, causando un problema imprevisto.
Per robustezza, di solito è meglio lavorare con buffer di dimensioni fisse, ove possibile, e progettare lo schizzo in modo che funzioni esplicitamente con quei limiti fin dall'inizio. Ciò significa che eventuali modifiche future allo schizzo o circostanze di runtime impreviste, si spera che non causino problemi di memoria.
Non sono d'accordo con le persone che pensano che non dovresti usarlo o che generalmente non è necessario. Credo che possa essere pericoloso se non si conoscono i dettagli, ma è utile. Ho dei casi in cui non conosco (e non dovrebbe interessarmi di sapere) le dimensioni di una struttura o di un buffer (in fase di compilazione o di esecuzione), specialmente quando si tratta di librerie che invio nel mondo. Sono d'accordo che se la tua applicazione ha a che fare solo con una struttura unica e nota, devi solo cuocere quella dimensione al momento della compilazione.
Esempio: ho una classe di pacchetti seriali (una libreria) che può accettare payload di dati di lunghezza arbitraria (può essere struct, array di uint16_t, ecc.). All'estremità di invio di quella classe devi semplicemente dire al metodo Packet.send () l'indirizzo della cosa che desideri inviare e la porta HardwareSerial attraverso la quale desideri inviarla. Tuttavia, sul lato ricevente ho bisogno di un buffer di ricezione allocato dinamicamente per contenere quel payload in entrata, poiché quel payload potrebbe essere una struttura diversa in qualsiasi momento, a seconda dello stato dell'applicazione, ad esempio. Se invio solo una singola struttura avanti e indietro, renderei il buffer della dimensione necessaria per essere compilato. Ma, nel caso in cui i pacchetti possano avere lunghezze diverse nel tempo, malloc () e free () non sono poi così male.
Ho eseguito test con il seguente codice per giorni, lasciandolo continuamente in loop e non ho trovato prove di frammentazione della memoria. Dopo aver liberato la memoria allocata dinamicamente, la quantità disponibile torna al valore precedente.
// 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
}
Non ho visto alcun tipo di degrado nella RAM o nella mia capacità di allocarlo in modo dinamico usando questo metodo, quindi direi che è uno strumento praticabile. FWIW.
È davvero una cattiva idea usare malloc () e free () con Arduino?
La risposta breve è sì. Di seguito sono riportati i motivi per cui:
Si tratta solo di capire cos'è una MPU e come programmare entro i limiti delle risorse disponibili. Arduino Uno utilizza una MPU ATmega328p con memoria flash ISP da 32 KB, EEPROM da 1024 B e SRAM da 2 KB. Non si tratta di molte risorse di memoria.
Ricorda che la SRAM da 2 KB viene utilizzata per tutte le variabili globali, i letterali di stringa, lo stack e il possibile utilizzo dell'heap. Lo stack deve anche avere spazio per un ISR.
Il layout della memoria è:
Oggi PC / laptop hanno una quantità di memoria superiore a 1.000.000 di volte. Uno spazio di stack predefinito di 1 Mbyte per thread non è raro ma totalmente irrealistico su una MPU.
Un progetto software incorporato deve fare un budget di risorse. Ciò sta stimando la latenza ISR, lo spazio di memoria necessario, la potenza di calcolo, i cicli di istruzione, ecc. Sfortunatamente non ci sono pranzi liberi e la programmazione integrata in tempo reale è la più difficile abilità di programmazione da padroneggiare.
Ok, so che questa è una vecchia domanda, ma più leggo le risposte, più continuo a tornare a un'osservazione che sembra saliente.
Sembra esserci un collegamento con il problema di arresto di Turing qui. Consentire un'allocazione dinamica aumenta le probabilità di detto "arresto", quindi la domanda diventa di tolleranza al rischio. Mentre è conveniente evitare la possibilità di malloc()
fallire e così via, è ancora un risultato valido. La domanda che l'OP pone sembra riguardare solo la tecnica e sì, i dettagli delle librerie utilizzate o la MPU specifica sono importanti; la conversazione si orienta verso la riduzione del rischio di interruzione del programma o di qualsiasi altra fine anomala. Dobbiamo riconoscere l'esistenza di ambienti che tollerano il rischio in modo molto diverso. Il mio progetto di hobby per mostrare bei colori su una striscia a LED non ucciderà qualcuno se succede qualcosa di insolito, ma probabilmente l'MCU all'interno di una macchina cuore-polmone lo farà.
Per la mia striscia LED, non mi importa se si blocca, lo resetterò e basta. Se fossi su una macchina cuore-polmone controllata da un MCU le conseguenze del suo blocco o mancato funzionamento sono letteralmente vita e morte, quindi la domanda su malloc()
e free()
dovrebbe essere divisa tra come il programma previsto affronta la possibilità di dimostrare Mr. Il famoso problema di Turing. Può essere facile dimenticare che si tratta di una prova matematica e di convincerci che se solo siamo abbastanza intelligenti possiamo evitare di essere una vittima dei limiti del calcolo.
Questa domanda dovrebbe avere due risposte accettate, una per coloro che sono costretti a sbattere le palpebre quando fissano The Halting Problem in faccia, e una per tutti gli altri. Mentre la maggior parte degli usi di Arduino non sono probabilmente applicazioni mission-critical o vita-e-morte, la distinzione è ancora lì indipendentemente da quale MPU si stia codificando.
No, ma devono essere usati con molta attenzione per quanto riguarda la memoria allocata libera (). Non ho mai capito perché la gente dice che la gestione diretta della memoria dovrebbe essere evitata in quanto implica un livello di incompetenza che è generalmente incompatibile con lo sviluppo del software.
Diciamo che stai usando il tuo arduino per controllare un drone. Qualsiasi errore in qualsiasi parte del codice potrebbe potenzialmente causare la sua caduta dal cielo e ferire qualcuno o qualcosa. In altre parole, se qualcuno non ha le competenze per usare malloc, probabilmente non dovrebbe codificare affatto poiché ci sono così tante altre aree in cui piccoli bug possono causare seri problemi.
Gli errori causati da Malloc sono più difficili da rintracciare e correggere? Sì, ma è più una questione di frustrazione da parte dei programmatori che di rischio. Per quanto riguarda il rischio, qualsiasi parte del tuo codice può essere uguale o più rischiosa di malloc se non fai i passi per assicurarti che sia fatto bene.