Usare malloc () e free () è davvero una pessima idea su Arduino?


49

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?


2
altrimenti esaurirai la memoria molto velocemente, e se sai quanta memoria utilizzerai potresti anche allocarla staticamente comunque
maniaco del cricchetto

1
Non so se sia male , ma penso che non sia usato perché non si esaurisce quasi mai la RAM per la maggior parte degli schizzi ed è solo uno spreco di flash e preziosi cicli di clock. Inoltre, non dimenticare l'ambito (anche se non so se quello spazio è ancora allocato per tutte le variabili).
Anonimo Penguin

4
Come al solito, la risposta giusta è "dipende". Non hai fornito informazioni sufficienti per sapere con certezza se l'allocazione dinamica è adatta a te.
WineSoaked

Risposte:


40

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.


Un'altra nota storica, questo ha portato rapidamente al segmento BSS, che ha permesso a un programma di azzerare la propria memoria per l'inizializzazione, senza copiare lentamente gli zeri durante il caricamento del programma.
rsaxvc,

16

In genere, quando si scrivono schizzi di Arduino, si eviterà l'allocazione dinamica (che si tratti di malloco newper istanze C ++), le persone preferiscono invece utilizzare le staticvariabili globali o variabili locali (stack).

L'uso dell'allocazione dinamica può comportare diversi problemi:

  • perdite di memoria (se si perde un puntatore a una memoria precedentemente allocata, o più probabilmente se si dimentica di liberare la memoria allocata quando non è più necessaria)
  • frammentazione dell'heap (dopo diverse malloc/ freechiamate) in cui l'heap diventa più grande della quantità effettiva di memoria allocata attualmente

Nella 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 Dummyclasse abbia una bufferdimensione 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 Dummyoggetti diversi con bufferdimensioni 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 buffervenga liberata quando Dummyun'istanza viene eliminata.


14

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:

1. Allocare solo buffer di lunga durata

Con questo intendo: allocare tutto il necessario all'inizio del programma e non liberarlo mai. Naturalmente, in questo caso, potresti anche usare buffer statici ...

2. Allocare solo buffer di breve durata

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.

3. Liberare sempre l'ultimo buffer allocato

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().

4. Allocare sempre la stessa dimensione

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.


1
Il pattern 2 dovrebbe essere evitato in quanto aggiunge cicli per malloc () e free () quando questo può essere fatto con "char buffer [size];" (in C ++). Vorrei anche aggiungere l'anti-pattern "Never from an ISR".
Mikael Patel,

9

L'uso dell'allocazione dinamica (tramite malloc/ freeo new/ delete) non è intrinsecamente negativo in quanto tale. In effetti, per qualcosa come l'elaborazione delle stringhe (ad esempio tramite l' Stringoggetto), è 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.


6

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.


2
Il codice di test è conforme al modello di utilizzo 2. Allocare solo buffer di breve durata che ho descritto nella mia risposta precedente. Questo è uno di quei pochi schemi di utilizzo noti per essere sicuri.
Edgar Bonet

In altre parole, i problemi sorgeranno quando inizi a condividere il processore con altro codice sconosciuto , che è precisamente il problema che pensi di evitare. In genere, se si desidera qualcosa che funzioni sempre o che fallisca durante il collegamento, si effettua un'allocazione fissa della dimensione massima e la si utilizza più e più volte, ad esempio facendo in modo che l'utente lo trasmetta all'inizializzazione. Ricorda che in genere stai eseguendo un chip in cui tutto deve contenere 2048 byte, forse di più su alcune schede ma forse anche molto meno su altre.
Chris Stratton,

@EdgarBonet Sì, esattamente. Volevo solo condividere.
StuffAndy Fa

1
L'allocazione dinamica di un buffer di solo le dimensioni necessarie è rischiosa, come se qualsiasi altra allocazione prima di liberarlo, si può rimanere con frammentazione, memoria che non è possibile riutilizzare. Inoltre, l'allocazione dinamica ha un overhead di tracciamento. L'allocazione fissa non significa che non puoi moltiplicare l'uso della memoria, significa solo che devi lavorare sulla condivisione nella progettazione del tuo programma. Per un buffer con ambito puramente locale, è inoltre possibile valutare l'utilizzo dello stack. Non hai nemmeno verificato la possibilità che malloc () fallisca.
Chris Stratton,

1
"può essere pericoloso se non conosci i dettagli, ma è utile." riassume praticamente tutto lo sviluppo in C / C ++. :-)
ThatAintWorking

4

È 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 è:

Mappa SRAM

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.


Amen a questo: "[H] ard programmazione in tempo reale è la più difficile delle abilità di programmazione da padroneggiare".
StuffAndy:

Il tempo di esecuzione di malloc è sempre lo stesso? Posso immaginare che malloc impieghi più tempo mentre cerca ulteriormente nella RAM disponibile uno slot adatto? Questo sarebbe l'ennesimo argomento (a parte l'esaurimento della RAM) per non allocare memoria in movimento?
Paul,

@Paul Gli algoritmi heap (malloc e free) in genere non sono tempi di esecuzione costanti e non rientrano. L'algoritmo contiene strutture di ricerca e dati che richiedono blocchi quando si utilizzano thread (concorrenza).
Mikael Patel

0

Ok, so che questa è una vecchia domanda, ma più leggo le risposte, più continuo a tornare a un'osservazione che sembra saliente.

Il problema dell'arresto è reale

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à.

Buongiorno Sig. Turing, mi chiamo Hubris

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.


Non penso che il problema di Halting si applichi in questa situazione specifica considerando il fatto che l'utilizzo dell'heap non è necessariamente arbitrario. Se utilizzato in modo ben definito, l'utilizzo dell'heap diventa prevedibilmente "sicuro". Il punto del problema di Halting è stato scoperto se si può determinare cosa succede a un algoritmo necessariamente arbitrario e non così ben definito. Si applica molto di più alla programmazione in senso lato e, come tale, trovo che qui non sia particolarmente pertinente. Non penso nemmeno che sia del tutto rilevante per essere del tutto onesto.
Jonathan Gray,

Devo ammettere un po 'di esagerazione retorica, ma il punto è davvero se si desidera garantire un comportamento, usando l'heap implica un livello di rischio che è molto più alto rispetto al solo usare lo stack.
Kelly S. francese,

-3

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.


4
È interessante che tu abbia usato un drone come esempio. Secondo questo articolo ( mil-embedded.com/articles/… ), "A causa del suo rischio, l'allocazione dinamica della memoria è vietata, in base allo standard DO-178B, nel codice avionico incorporato critico per la sicurezza".
Gabriel Staples,

DARPA ha una lunga storia nel consentire agli appaltatori di sviluppare specifiche adatte alla propria piattaforma - perché non dovrebbero farlo quando sono i contribuenti a pagare il conto. Questo è il motivo per cui costa $ 10 miliardi per loro di sviluppare ciò che gli altri possono fare con $ 10.000. Sembra quasi che tu stia usando il complesso industriale militare come riferimento onesto.
JSON,

L'allocazione dinamica sembra un invito per il tuo programma a dimostrare i limiti di calcolo descritti in Halting Problem. Ci sono alcuni ambienti in grado di gestire una piccola quantità di rischio di tale arresto e esistono ambienti (spazio, difesa, medicina, ecc.) Che non tollerano alcuna quantità di rischio controllabile, quindi non consentono operazioni che "non dovrebbero" fallire perché "dovrebbe funzionare" non è abbastanza buono quando si lancia un razzo o si controlla una macchina cuore / polmone.
Kelly S. francese,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.