Czy używanie malloc () i free () to naprawdę zły pomysł na Arduino?


49

Używanie malloc()i free()wydaje się dość rzadkie w świecie Arduino. Jest stosowany w czystym AVR C znacznie częściej, ale nadal ostrożnie.

Czy to naprawdę zły pomysł do wykorzystania malloc()i free()z Arduino?


2
w przeciwnym razie zabraknie ci pamięci bardzo szybko, a jeśli wiesz, ile pamięci zużyjesz, równie dobrze możesz ją alokować statycznie
maniak ratchet

1
Nie wiem, czy jest źle , ale myślę, że nie jest używany, ponieważ prawie nigdy nie brakuje pamięci RAM dla większości szkiców, a to tylko marnotrawstwo lampy błyskowej i cennych cykli zegara. Nie zapomnij także o zakresie (chociaż nie wiem, czy to miejsce jest nadal przydzielone dla wszystkich zmiennych).
Anonimowy pingwin

4
Jak zwykle poprawną odpowiedzią jest „to zależy”. Nie podałeś wystarczającej ilości informacji, aby mieć pewność, że alokacja dynamiczna jest dla Ciebie odpowiednia.
WineSoaked

Odpowiedzi:


40

Moją ogólną zasadą dla systemów wbudowanych jest tylko malloc()duże bufory i tylko raz, na początku programu, np setup(). W. Problem pojawia się, gdy przydzielasz i zwalniasz pamięć. Podczas sesji długookresowej pamięć ulega fragmentacji i ostatecznie alokacja kończy się niepowodzeniem z powodu braku wystarczająco dużego wolnego miejsca, mimo że całkowita wolna pamięć jest więcej niż wystarczająca dla żądania.

(Perspektywa historyczna, pomiń, jeśli nie jesteś zainteresowany): W zależności od implementacji modułu ładującego, jedyną zaletą alokacji w czasie wykonywania w porównaniu z alokacją w czasie kompilacji (zinicjalizowane globały) jest rozmiar pliku szesnastkowego. Kiedy budowano systemy wbudowane z gotowymi komputerami posiadającymi całą pamięć ulotną, program często był przesyłany do systemu wbudowanego z sieci lub komputera oprzyrządowania, a czasem przesyłania był czasem problem. Pozostawienie buforów pełnych zer na obrazie może znacznie skrócić czas.)

Jeśli potrzebuję dynamicznej alokacji pamięci w systemie wbudowanym, generalnie malloc(), a najlepiej statycznie, alokuję dużą pulę i dzielę ją na bufory o stałej wielkości (lub jedną pulę, odpowiednio, z małych i dużych buforów) i wykonuję własną alokację / alokacja z tej puli. Następnie każde żądanie dowolnej ilości pamięci do ustalonego rozmiaru bufora jest honorowane za pomocą jednego z tych buforów. Funkcja wywołująca nie musi wiedzieć, czy jest większa niż żądana, a unikając podziału i ponownego łączenia bloków rozwiązujemy fragmentację. Oczywiście przecieki pamięci mogą nadal występować, jeśli program ma błędy alokacji / alokacji.


Kolejna historyczna uwaga, która szybko doprowadziła do segmentu BSS, co pozwoliło programowi wyzerować własną pamięć do inicjalizacji, bez powolnego kopiowania zer podczas ładowania programu.
rsaxvc

16

Zazwyczaj, pisząc szkice Arduino, unikniesz dynamicznej alokacji (czy to z instancjami C ++, mallocczy newdla nich), ludzie raczej używają globalnych staticzmiennych lub zmiennych lokalnych lub zmiennych stosu.

Korzystanie z alokacji dynamicznej może prowadzić do kilku problemów:

  • wycieki pamięci (jeśli zgubisz wskaźnik do wcześniej przydzielonej pamięci lub bardziej prawdopodobne, jeśli zapomnisz zwolnić przydzieloną pamięć, gdy już jej nie potrzebujesz)
  • kupie fragmentacja (po kilku malloc/ freeprzychodzące), gdzie sterta rośnie większy Thant rzeczywistej ilości pamięci przydzielonej obecnie

W większości sytuacji, z którymi się spotkałem, alokacja dynamiczna albo nie była konieczna, albo można jej uniknąć za pomocą makr, jak w poniższym przykładzie kodu:

MySketch.ino

#define BUFFER_SIZE 32
#include "Dummy.h"

Dummy.h

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

Bez tego #define BUFFER_SIZE, jeśli chcielibyśmy, aby Dummyklasa miała nieokreślony bufferrozmiar, musielibyśmy zastosować dynamiczny przydział w następujący sposób:

class Dummy
{
    const byte* buffer;

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

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

W tym przypadku mamy więcej opcji niż w pierwszej próbce (np. Używamy różnych Dummyobiektów o różnych bufferrozmiarach dla każdej z nich), ale możemy mieć problemy z fragmentacją sterty.

Uwaga: użycie destruktora w celu zapewnienia, że ​​pamięć przydzielana dynamicznie bufferzostanie zwolniona po Dummyusunięciu instancji.


14

malloc()Przyjrzałem się używanemu algorytmowi z avr-libc i wydaje się, że istnieje kilka wzorców użycia, które są bezpieczne z punktu widzenia fragmentacji sterty:

1. Przydzielaj tylko bufory długotrwałe

Rozumiem przez to: przydziel wszystko, czego potrzebujesz na początku programu i nigdy go nie zwalniaj. Oczywiście w tym przypadku równie dobrze można użyć buforów statycznych ...

2. Przydzielaj tylko bufory krótkotrwałe

Czyli: zwalniasz bufor przed przydzieleniem czegokolwiek innego. Rozsądny przykład może wyglądać następująco:

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

Jeśli w środku nie ma malloc do_whatever_with()lub jeśli ta funkcja uwalnia to, co przydzieli, to jesteś bezpieczny przed fragmentacją.

3. Zawsze zwalniaj ostatni przydzielony bufor

Jest to uogólnienie dwóch poprzednich przypadków. Jeśli użyjesz sterty jak stosu (ostatnie wejście jest pierwsze wyjęte), zachowa się jak stos, a nie fragment. Należy zauważyć, że w tym przypadku można bezpiecznie zmienić rozmiar ostatnio przydzielonego bufora za pomocą realloc().

4. Zawsze przydzielaj ten sam rozmiar

Nie zapobiegnie to fragmentacji, ale jest bezpieczne w tym sensie, że sterta nie wzrośnie powyżej maksymalnego używanego rozmiaru. Jeśli wszystkie Twoje bufory mają ten sam rozmiar, możesz być pewien, że za każdym razem, gdy zwolnisz jeden z nich, miejsce będzie dostępne dla kolejnych przydziałów.


1
Wzorca 2 należy unikać, ponieważ dodaje on cykle dla malloc () i free (), gdy można to zrobić za pomocą „char buffer [size];” (w C ++). Chciałbym również dodać anty-wzorzec „Nigdy od ISR”.
Mikael Patel

9

Korzystanie z alokacji dynamicznej (przez malloc/ freelub new/ delete) nie jest z natury złe. W rzeczywistości w przypadku przetwarzania ciągów znaków (np. Przez Stringobiekt) jest to często bardzo pomocne. Jest tak, ponieważ wiele szkiców używa kilku małych fragmentów ciągów, które ostatecznie łączą się w większe. Korzystanie z alokacji dynamicznej pozwala zużywać tylko tyle pamięci, ile potrzebujesz na każdą z nich. W przeciwieństwie do tego, użycie stałego bufora statycznego dla każdego z nich może spowodować marnowanie dużej ilości miejsca (powodując, że zabraknie mu pamięci znacznie szybciej), chociaż zależy to całkowicie od kontekstu.

Biorąc to wszystko pod uwagę, bardzo ważne jest, aby upewnić się, że użycie pamięci jest przewidywalne. Umożliwienie szkicowi użycia dowolnej ilości pamięci w zależności od okoliczności w czasie wykonywania (np. Danych wejściowych) może łatwo spowodować problem wcześniej czy później. W niektórych przypadkach może być całkowicie bezpieczny, np. Jeśli wiesz, że użycie nigdy nie będzie miało większego znaczenia. Szkice mogą się jednak zmieniać podczas procesu programowania. Założenie przyjęte wcześnie można zapomnieć, gdy coś zostanie później zmienione, co spowoduje nieprzewidziany problem.

Aby uzyskać solidność, zwykle lepiej jest pracować z buforami o stałym rozmiarze, jeśli to możliwe, i zaprojektować szkic, aby od samego początku działał wyraźnie z tymi ograniczeniami. Oznacza to, że wszelkie przyszłe zmiany w szkicu lub wszelkie nieoczekiwane okoliczności w czasie wykonywania nie powinny powodować problemów z pamięcią.


6

Nie zgadzam się z ludźmi, którzy uważają, że nie powinieneś go używać lub jest to na ogół niepotrzebne. Uważam, że może być niebezpieczne, jeśli nie znasz jego tajników, ale jest to przydatne. Mam przypadki, w których nie znam (i nie powinienem wiedzieć) rozmiaru struktury lub bufora (w czasie kompilacji lub w czasie wykonywania), szczególnie jeśli chodzi o biblioteki wysyłane na świat. Zgadzam się, że jeśli Twoja aplikacja zajmuje się tylko jedną znaną strukturą, powinieneś po prostu upiec ten rozmiar w czasie kompilacji.

Przykład: Mam klasę pakietu szeregowego (bibliotekę), która może przyjmować ładunki danych o dowolnej długości (może to być struct, tablica uint16_t itp.). Na końcu wysyłającym tej klasy po prostu podajesz metodzie Packet.send () adres rzeczy, którą chcesz wysłać, oraz port HardwareSerial, przez który chcesz ją wysłać. Jednak po stronie odbiorczej potrzebuję dynamicznie przydzielanego bufora odbiorczego, aby utrzymać ten przychodzący ładunek, ponieważ ładunek ten może mieć inną strukturę w dowolnym momencie, na przykład w zależności od stanu aplikacji. JEŚLI kiedykolwiek wysyłam tylko jedną strukturę tam iz powrotem, po prostu ustawię rozmiar bufora, jaki powinien być w czasie kompilacji. Ale w przypadku, gdy pakiety mogą mieć różne długości w czasie, malloc () i free () nie są takie złe.

Przez kilka dni testowałem następujący kod, pozwalając mu na ciągłe zapętlenie i nie znalazłem żadnych dowodów na fragmentację pamięci. Po zwolnieniu pamięci przydzielanej dynamicznie wolna ilość powraca do poprzedniej wartości.

// 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
}

Nie widziałem żadnej degradacji pamięci RAM ani mojej zdolności do dynamicznego przydzielania jej za pomocą tej metody, więc powiedziałbym, że jest to realne narzędzie. FWIW.


2
Twój kod testowy jest zgodny ze wzorem użycia 2. Przydzielaj tylko krótkotrwałe bufory, które opisałem w mojej poprzedniej odpowiedzi. Jest to jeden z niewielu wzorców użytkowania, o których wiadomo, że są bezpieczne.
Edgar Bonet

Innymi słowy, problemy pojawią się, gdy zaczniesz współużytkować procesor z innym nieznanym kodem - właśnie takiego problemu możesz uniknąć. Zasadniczo, jeśli chcesz czegoś, co zawsze będzie działało lub zawiedzie podczas łączenia, dokonujesz stałej alokacji maksymalnego rozmiaru i używasz go w kółko, na przykład poprzez przekazanie go użytkownikowi podczas inicjalizacji. Pamiętaj, że zazwyczaj pracujesz na układzie, w którym wszystko musi mieścić się w 2048 bajtach - może więcej na niektórych płytach, ale może też dużo mniej na innych.
Chris Stratton

@EdgarBonet Tak, dokładnie. Chciałem się tylko podzielić.
StuffAndyMakes

1
Dynamiczne przydzielanie bufora tylko wymaganego rozmiaru jest ryzykowne, ponieważ jeśli cokolwiek innego przydzieli przed zwolnieniem, możesz zostać podzielony na fragmenty - pamięć, której nie możesz ponownie użyć. Ponadto dynamiczne przydzielanie ma narzut związany z śledzeniem. Stały przydział nie oznacza, że ​​nie można wielokrotnie korzystać z pamięci, to po prostu oznacza, że ​​musisz przećwiczyć udostępnianie w projekcie swojego programu. W przypadku bufora o zasięgu wyłącznie lokalnym możesz również rozważyć użycie stosu. Nie sprawdziłeś również możliwości niepowodzenia malloc ().
Chris Stratton

1
„może być niebezpieczne, jeśli nie znasz jego tajników, ale jest przydatne”. prawie podsumowuje cały rozwój w C / C ++. :-)
ThatAintWorking

4

Czy naprawdę złym pomysłem jest używanie malloc () i free () w Arduino?

Krótka odpowiedź brzmi: tak. Poniżej znajdują się powody, dla których:

Chodzi przede wszystkim o zrozumienie, czym jest MPU i jak programować w ramach ograniczeń dostępnych zasobów. Arduino Uno wykorzystuje MPU ATmega328p z pamięcią flash 32KB ISP, EEPROM 1024B i SRAM 2KB. To niewiele zasobów pamięci.

Pamiętaj, że 2KB SRAM jest używany do wszystkich zmiennych globalnych, literałów łańcuchowych, stosu i możliwego użycia sterty. Na stosie musi również znajdować się miejsce dla ISR.

Układ pamięci to:

Mapa SRAM

Dzisiejsze komputery PC / laptopy mają ponad 1.000.000 razy więcej pamięci. Domyślne miejsce na stosie 1 MB na wątek nie jest rzadkie, ale całkowicie nierealne w MPU.

Projekt oprogramowania wbudowanego musi mieć budżet zasobów. Szacuje się opóźnienia ISR, niezbędną przestrzeń pamięci, moc obliczeniową, cykle instrukcji itp. Niestety nie ma żadnych wolnych obiadów, a trudne wbudowane programowanie w czasie rzeczywistym jest najtrudniejszą z umiejętności programowania.


W związku z tym: „Wbudowane programowanie w czasie rzeczywistym jest najtrudniejsze do opanowania.”
StuffAndyMakes

Czy czas realizacji Malloc jest zawsze taki sam? Mogę sobie wyobrazić, że Malloc zajmuje więcej czasu, gdy szuka dalej w dostępnym pamięci RAM w celu znalezienia pasującego gniazda? Byłby to kolejny argument (oprócz braku pamięci RAM), aby nie przydzielać pamięci w podróży?
Paul

@Paul Algorytmy sterty (malloc i free) zazwyczaj nie są stałym czasem wykonania i nie są wysyłane ponownie. Algorytm zawiera struktury wyszukiwania i danych, które wymagają blokad podczas używania wątków (współbieżność).
Mikael Patel

0

Ok, wiem, że to stare pytanie, ale im więcej czytam odpowiedzi, tym bardziej wracam do obserwacji, która wydaje się istotna.

Problem zatrzymania jest prawdziwy

Wygląda na to, że istnieje tutaj związek z problemem zatrzymania Turinga. Zezwolenie na alokację dynamiczną zwiększa szanse na wspomniane „zatrzymanie”, dlatego pojawia się pytanie o tolerancję ryzyka. Chociaż wygodnie jest uchylić się od możliwości malloc()niepowodzenia i tak dalej, nadal jest to ważny wynik. Pytanie, które zadaje OP, wydaje się dotyczyć wyłącznie techniki, i tak, szczegóły używanych bibliotek lub konkretnego MPU mają znaczenie; rozmowa zmierza w kierunku zmniejszenia ryzyka zatrzymania programu lub innego nienormalnego zakończenia. Musimy rozpoznać istnienie środowisk, które tolerują ryzyko znacznie inaczej. Mój projekt hobby polegający na wyświetlaniu ładnych kolorów na pasku LED nie zabije kogoś, jeśli wydarzy się coś niezwykłego, ale prawdopodobnie MCU w maszynie płuco-serce to zrobi.

Cześć, panie Turing. Nazywam się Hubris

W przypadku paska LED nie obchodzi mnie, czy się zablokuje, po prostu go zresetuję. Gdybym był na maszynie płuco-serce kontrolowanej przez MCU, konsekwencjami jego zablokowania lub braku działania są dosłownie życie i śmierć, więc pytanie o malloc()i free()powinno być podzielone między to, jak zamierzony program radzi sobie z możliwością zademonstrowania pana Słynny problem Turinga. Łatwo zapomnieć, że jest to matematyczny dowód i przekonać się, że jeśli tylko jesteśmy wystarczająco sprytni, możemy uniknąć bycia ofiarą ograniczeń obliczeniowych.

To pytanie powinno mieć dwie zaakceptowane odpowiedzi, jedną dla tych, którzy zmuszeni są mrugać, wpatrując się w problem zatrzymania w twarz, i drugą dla wszystkich pozostałych. Chociaż większość zastosowań arduino prawdopodobnie nie jest krytyczna dla misji lub aplikacji na śmierć i życie, wciąż istnieje rozróżnienie, niezależnie od tego, którą MPU kodujesz.


Nie sądzę, aby problem zatrzymania miał zastosowanie w tej konkretnej sytuacji, biorąc pod uwagę fakt, że użycie sterty niekoniecznie jest arbitralne. Jeśli zostanie użyta w dobrze zdefiniowany sposób, użycie sterty staje się przewidywalnie „bezpieczne”. Istotą problemu Haltinga było ustalenie, czy można ustalić, co dzieje się z koniecznie arbitralnym i niezbyt dobrze zdefiniowanym algorytmem. To naprawdę dotyczy znacznie więcej programowania w szerszym znaczeniu i jako takie uważam, że nie ma tutaj szczególnego znaczenia. Szczerze mówiąc, nie sądzę, żeby to było w ogóle istotne.
Jonathan Gray

Przyznaję się do retorycznej przesady, ale tak naprawdę chodzi o to, że jeśli chcesz zagwarantować zachowanie, użycie stosu oznacza poziom ryzyka znacznie wyższy niż trzymanie się samego stosu.
Kelly S. French

-3

Nie, ale należy ich używać bardzo ostrożnie w odniesieniu do zwolnienia () przydzielonej pamięci. Nigdy nie zrozumiałem, dlaczego ludzie twierdzą, że należy unikać bezpośredniego zarządzania pamięcią, ponieważ implikuje to poziom niekompetencji, który zasadniczo jest niezgodny z tworzeniem oprogramowania.

Powiedzmy, że używasz swojego arduino do kontrolowania drona. Każdy błąd w dowolnej części kodu może potencjalnie spowodować jego wypadnięcie z nieba i zranienie kogoś lub czegoś. Innymi słowy, jeśli ktoś nie ma umiejętności korzystania z malloc, prawdopodobnie nie powinien w ogóle kodować, ponieważ istnieje tak wiele innych obszarów, w których małe błędy mogą powodować poważne problemy.

Czy błędy spowodowane przez malloc są trudniejsze do wyśledzenia i naprawienia? Tak, ale to bardziej frustracja ze strony programistów niż ryzyko. Jeśli chodzi o ryzyko, każda część kodu może być równie ryzykowna lub bardziej ryzykowna niż malloc, jeśli nie podejmiesz kroków w celu upewnienia się, że zostało wykonane poprawnie.


4
Ciekawe, że użyłeś drona jako przykładu. Zgodnie z tym artykułem ( mil-embedded.com/articles/… ) „Ze względu na ryzyko dynamiczne przydzielanie pamięci jest zabronione, zgodnie ze standardem DO-178B, w krytycznym dla bezpieczeństwa wbudowanym kodzie awioniki”.
Gabriel Staples

DARPA ma długą historię umożliwiania wykonawcom opracowywania specyfikacji pasujących do ich własnej platformy - dlaczego nie mieliby tego robić, skoro to podatnicy płacą rachunek. Dlatego kosztuje ich 10 miliardów dolarów na opracowanie tego, co inni mogą zrobić za pomocą 10 000 dolarów. Prawie brzmi, jakbyś użył wojskowego kompleksu przemysłowego jako uczciwego odniesienia.
JSON

Alokacja dynamiczna wydaje się zaproszeniem do przedstawienia przez program ograniczeń obliczeniowych opisanych w Problemie zatrzymania. Istnieje kilka środowisk, które mogą poradzić sobie z niewielkim ryzykiem takiego zatrzymania, i istnieją środowiska (kosmiczne, obronne, medyczne itp.), Które nie będą tolerować żadnego możliwego do kontrolowania ryzyka, dlatego nie pozwalają na operacje, które „nie powinny” zawodzi, ponieważ „powinno działać” nie jest wystarczająco dobre, gdy wystrzeliwujesz rakietę lub kontrolujesz maszynę serca / płuc.
Kelly S. French
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.