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?
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?
Odpowiedzi:
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.
Zazwyczaj, pisząc szkice Arduino, unikniesz dynamicznej alokacji (czy to z instancjami C ++, malloc
czy new
dla nich), ludzie raczej używają globalnych static
zmiennych lub zmiennych lokalnych lub zmiennych stosu.
Korzystanie z alokacji dynamicznej może prowadzić do kilku problemów:
malloc
/ free
przychodzące), gdzie sterta rośnie większy Thant rzeczywistej ilości pamięci przydzielonej obecnieW 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 Dummy
klasa miała nieokreślony buffer
rozmiar, 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 Dummy
obiektów o różnych buffer
rozmiarach dla każdej z nich), ale możemy mieć problemy z fragmentacją sterty.
Uwaga: użycie destruktora w celu zapewnienia, że pamięć przydzielana dynamicznie buffer
zostanie zwolniona po Dummy
usunięciu instancji.
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:
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 ...
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ą.
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()
.
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.
Korzystanie z alokacji dynamicznej (przez malloc
/ free
lub new
/ delete
) nie jest z natury złe. W rzeczywistości w przypadku przetwarzania ciągów znaków (np. Przez String
obiekt) 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ą.
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.
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:
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.
Ok, wiem, że to stare pytanie, ale im więcej czytam odpowiedzi, tym bardziej wracam do obserwacji, która wydaje się istotna.
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.
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, 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.