Płyta Arduino Uno ma ograniczoną pamięć RAM, co oznacza, że ma ograniczony stos połączeń. Czasami rekurencja jest jedyną szybką opcją do implementacji określonego algorytmu. Więc biorąc pod uwagę, że stos wywołań jest poważnie ograniczony, jaki byłby sposób, aby dowiedzieć się, że biorąc pod uwagę pewien program działający na płycie, ile dokładnie wywołań rekurencyjnych możesz sobie pozwolić, zanim nastąpi przepełnienie stosu (i zdarzają się złe rzeczy)?
programming
sram
asheeshr
źródło
źródło
How much ca!@#QFSD@$RFW
? Zastanawiam się, dlaczego nikt tego nie zredagował, aby był bardziej znaczący (w ciągu ostatnich 4 lat).211
razy (w zależności od wielu czynników) :). Zobacz moją odpowiedź tutaj: arduino.stackexchange.com/a/51098/7727 . @NickGammon, chyba udaje, że „przeklina”. Jest to gra słów „rekurencja”. Zajęło mi to również chwilę, żeby to zrozumieć. Na początku było dość mylące.Odpowiedzi:
Jeśli naprawdę chcesz się powtórzyć (i jak powiedział @jippie, to zły pomysł; wiadomość podprogowa: nie rób tego ) i chcesz wiedzieć, ile możesz powtórzyć, będziesz musiał wykonać pewne obliczenia i eksperymenty; również na ogół będzie to tylko przybliżenie, ponieważ zależy ono w dużej mierze od stanu pamięci w momencie wywołania funkcji rekurencyjnej.
W tym celu najpierw powinieneś wiedzieć, jak SRAM jest zorganizowany w Arduino opartym na AVR (nie będzie to dotyczyło np. Arduino Galileo firmy Intel). Poniższy schemat z Adafruit pokazuje to wyraźnie:
Następnie musisz znać całkowity rozmiar SRAM (zależy od MCU Atmela, stąd jaki rodzaj płyty Arduino posiadasz).
Na tym diagramie łatwo jest ustalić rozmiar bloku danych statycznych, jaki jest znany w czasie kompilacji i nie zmieni się później.
Rozmiar sterty może być trudniejszy do ustalenia, ponieważ może się różnić w czasie wykonywania, w zależności od dynamicznych przydziałów pamięci (
malloc
lubnew
) wykonywanych przez szkic lub używane biblioteki. Używanie pamięci dynamicznej jest dość rzadkie w Arduino, ale robią to niektóre standardowe funkcje (String
myślę, że używa tego typu ).W przypadku rozmiaru stosu będzie się on również zmieniać w czasie wykonywania, w zależności od bieżącej głębokości wywołań funkcji (każde wywołanie funkcji zajmuje 2 bajty na stosie do przechowywania adresu wywołującego) oraz liczby i wielkości zmiennych lokalnych, w tym przekazanych argumentów ( które są również przechowywane na stosie ) dla wszystkich wywoływanych do tej pory funkcji.
Załóżmy więc, że twoja
recurse()
funkcja używa 12 bajtów dla swoich lokalnych zmiennych i argumentów, a następnie każde wywołanie tej funkcji (pierwsze wywołanie zewnętrzne i rekurencyjne) użyje12+2
bajtów.Jeśli przypuszczamy, że:
recurse()
funkcja jest wywoływana ze szkicu, bieżący stos ma 128 bajtów długościPozostają ci
2048 - 132 - 128 = 1788
dostępne bajty na stosie . Dlatego liczba wywołań rekurencyjnych do Twojej funkcji1788 / 14 = 127
, w tym wywołanie początkowe (które nie jest rekurencyjne).Jak widać, znalezienie tego, czego chcesz, jest bardzo trudne, ale nie niemożliwe.
Prostszym sposobem na uzyskanie dostępnego wcześniej rozmiaru stosu
recurse()
byłoby użycie następującej funkcji (znalezionej w centrum szkoleniowym Adafruit; sam tego nie testowałem):Gorąco zachęcam do przeczytania tego artykułu w centrum szkoleniowym Adafruit.
źródło
.bss
reprezentuje zmienne globalne bez wartości początkowej w kodzie, natomiastdata
dotyczy zmiennych globalnych z wartością początkową. Ale w końcu używają tej samej przestrzeni: Dane statyczne na schemacie.static
w funkcji.Rekurencja jest złą praktyką na mikrokontrolerze, ponieważ sam już to powiedziałeś i prawdopodobnie chcesz tego uniknąć, gdy tylko jest to możliwe. Na stronie Arduino jest kilka przykładów i bibliotek dostępnych do sprawdzania wielkości wolnej pamięci RAM . Możesz na przykład użyć tego, aby dowiedzieć się, kiedy przerwać rekurencję lub nieco trudniej / ryzykować, aby profilować swój szkic i nałożyć na niego sztywny kod. Ten profil byłby wymagany przy każdej zmianie w twoim programie i każdej zmianie w łańcuchu narzędzi Arduino.
źródło
To zależy od funkcji.
Za każdym razem, gdy wywoływana jest funkcja, nowa ramka jest wypychana na stos. Zwykle będzie zawierać różne krytyczne elementy, w tym potencjalnie:
this
) w przypadku wywołania funkcji członka.Jak widać, przestrzeń stosu wymagana dla danego wywołania zależy od funkcji. Na przykład, jeśli napiszesz funkcję rekurencyjną, która bierze tylko
int
parametr i nie używa zmiennych lokalnych, nie będzie potrzebować więcej niż kilku bajtów na stosie. Oznacza to, że możesz rekurencyjnie nazywać to znacznie więcej niż funkcją, która bierze kilka parametrów i używa wielu lokalnych zmiennych (które pochłaniają stos znacznie szybciej).Oczywiście stan stosu zależy od tego, co dzieje się w kodzie. Jeśli rozpoczniesz rekurencję bezpośrednio w
loop()
funkcji standardowej , prawdopodobnie na stosie nie będzie już dużo. Jeśli jednak zaczniesz, zagnieżdżony kilka poziomów głęboko w innych funkcjach, nie będzie już tyle miejsca. Wpłynie to na liczbę powtórzeń bez wyczerpania stosu.Warto zauważyć, że optymalizacja rekurencji ogona istnieje w niektórych kompilatorach (chociaż nie jestem pewien, czy avr-gcc to obsługuje). Jeśli wywołanie rekurencyjne jest ostatnią rzeczą w funkcji, oznacza to, że czasami można w ogóle uniknąć zmiany ramki stosu. Kompilator może po prostu ponownie wykorzystać istniejącą ramkę, ponieważ wywołanie „rodzicielskie” (że tak powiem) zostało z niego zakończone. Oznacza to, że możesz teoretycznie kontynuować rekurencję tak długo, jak chcesz, o ile twoja funkcja nie wywołuje niczego innego.
źródło
Miałem dokładnie to samo pytanie, co podczas czytania Jumping into C ++ autorstwa Alexa Allaina , Ch 16: Recursion, str. 230, więc przeprowadziłem kilka testów.
TLDR;
Mój Arduino Nano (ATmega328 mcu) może wykonywać 211 wywołań funkcji rekurencyjnych (dla kodu podanego poniżej), zanim przepełni się stos i zawiesi.
Po pierwsze, pozwól mi zająć się tym roszczeniem:
[Aktualizacja: ah, przejrzałem słowo „szybki”. W takim przypadku masz pewną ważność. Niemniej jednak uważam, że warto powiedzieć, co następuje.]
Nie, nie sądzę, żeby to było prawdziwe stwierdzenie. Jestem pewien, że wszystkie algorytmy mają zarówno rekurencyjne, jak i nierekurencyjne rozwiązanie, bez wyjątku. Po prostu czasami jest to znacznie łatwiejszeużyć algorytmu rekurencyjnego. Powiedziawszy to, rekursja jest bardzo niezadowolona z zastosowania w mikrokontrolerach i prawdopodobnie nigdy nie będzie dozwolona w kodzie krytycznym dla bezpieczeństwa. Niemniej jednak można to oczywiście zrobić na mikrokontrolerach. Aby wiedzieć, jak „głęboki” możesz przejść do dowolnej funkcji rekurencyjnej, po prostu ją przetestuj! Uruchom go w swojej rzeczywistej aplikacji w prawdziwym przypadku testowym i usuń podstawowy warunek, aby nieskończenie powrócił. Wydrukuj licznik i przekonaj się, jak „głęboki” możesz przejść, abyś wiedział, czy Twój algorytm rekurencyjny przesuwa granice pamięci RAM zbyt blisko, aby można go było praktycznie wykorzystać. Oto przykład poniżej, aby wymusić przepełnienie stosu na Arduino.
Teraz kilka notatek:
Liczba wywołań rekurencyjnych lub „ramek stosu”, które można uzyskać, zależy od wielu czynników, w tym:
free_RAM = total_RAM - stack_used - heap_used
lub możesz powiedziećfree_RAM = stack_size_allocated - stack_size_used
)Moje wyniki:
Segmentation fault (core dumped)
#pragma GCC optimize ("-O0")
na górę pliku i wykonaj ponownie:Here are the final print results: 209 210 211 ⸮ 9⸮ 3⸮
Kod:
Aplikacja na PC:
Program Arduino „Sketch”:
Bibliografia:
#pragma GCC optimize
rozkazem, ponieważ wiedziałem, że go tam udokumentowałem.źródło
#pragma
używanie tam. Zamiast tego możesz dodać__attribute__((optimize("O0")))
do pojedynczej funkcji, którą chcesz zoptymalizować.Napisałem ten prosty program testowy:
Skompilowałem go dla Uno i jak piszę, powrócił ponad milion razy! Nie wiem, ale kompilator mógł zoptymalizować ten program
źródło
call xxx
/ret
przezjmp xxx
. Sprowadza się to do tej samej rzeczy, z tym wyjątkiem, że metoda kompilatora nie zużywa stosu. W ten sposób możesz powtórzyć miliardy razy z twoim kodem (inne rzeczy są równe).#pragma GCC optimize ("-O0")
do górnej części programu Arduino. Uważam, że musisz to zrobić u góry każdego pliku, do którego chcesz go zastosować - ale nie szukałem tego od lat, więc sprawdź to na własne oczy.