Die Arduino Uno-Karte verfügt über einen begrenzten Arbeitsspeicher, was bedeutet, dass ein begrenzter Aufrufstapel verfügbar ist. Manchmal ist die Rekursion die einzige schnelle Option, um einen bestimmten Algorithmus zu implementieren. Wenn der Aufrufstapel stark eingeschränkt ist, wie kann man dann feststellen, dass bei einem bestimmten Programm auf der Platine genau wie viele rekursive Aufrufe möglich sind, bevor ein Stapelüberlauf auftritt (und schlimme Dinge passieren)?
programming
sram
Ascheshr
quelle
quelle
How much ca!@#QFSD@$RFW
:? Ich bin gespannt, warum das in den letzten 4 Jahren von niemandem sinnvoller herausgearbeitet wurde.211
mal (abhängig von vielen Faktoren) :). Siehe meine Antwort hier: arduino.stackexchange.com/a/51098/7727 . @ NickGammon, er tut so als ob er "flucht", denke ich. Es ist ein Wortspiel für "Rekurs". Ich habe eine Minute gebraucht, um das herauszufinden. War anfangs ziemlich verwirrend.Antworten:
Wenn Sie wirklich wiederkehren möchten (und wie @jippie sagte, ist es eine schlechte Idee; unterschwellige Meldung: Tun Sie es nicht ) und wissen möchten, wie viel Sie wiederkehren können, müssen Sie einige Berechnungen und Experimente durchführen. Außerdem haben Sie im Allgemeinen nur eine Annäherung, da dies stark vom Speicherstatus zum Zeitpunkt des Aufrufs Ihrer rekursiven Funktion abhängt.
Zu diesem Zweck sollten Sie zunächst wissen, wie SRAM in AVR-basiertem Arduino organisiert ist (dies gilt beispielsweise nicht für Arduino Galileo von Intel). Das folgende Diagramm von Adafruit zeigt es deutlich:
Dann müssen Sie die Gesamtgröße Ihres SRAM kennen (hängt von der Atmel-MCU ab, daher welche Art von Arduino-Karte Sie haben).
In diesem Diagramm ist es einfach, die Größe des statischen Datenblocks zu ermitteln, da dieser zur Kompilierungszeit bekannt ist und sich später nicht mehr ändert.
Die Größe des Heapspeichers kann schwieriger zu ermitteln sein, da sie zur Laufzeit variieren kann, abhängig von den dynamischen Speicherzuweisungen (
malloc
odernew
), die von Ihrer Skizze oder den verwendeten Bibliotheken durchgeführt werden. Die Verwendung von dynamischem Speicher ist auf Arduino recht selten, aber einige Standardfunktionen tun dies (String
ich denke, Typ verwendet ihn).Die Stack- Größe variiert auch während der Laufzeit, basierend auf der aktuellen Tiefe der Funktionsaufrufe (jeder Funktionsaufruf benötigt 2 Byte im Stack, um die Adresse des Aufrufers zu speichern) und der Anzahl und Größe der lokalen Variablen einschließlich der übergebenen Argumente ( die auch auf dem Stack gespeichert sind ) für alle bisher aufgerufenen Funktionen.
Nehmen wir also an, Ihre
recurse()
Funktion verwendet 12 Bytes für ihre lokalen Variablen und Argumente, und jeder Aufruf dieser Funktion (der erste von einem externen Aufrufer und der rekursive) verwendet12+2
Bytes.Wenn wir das annehmen:
recurse()
Funktion aus Ihrer Skizze aufgerufen wird, ist der aktuelle Stapel 128 Byte langDann verbleiben Ihnen die
2048 - 132 - 128 = 1788
verfügbaren Bytes auf dem Stack . Die Anzahl der rekursiven Aufrufe Ihrer Funktion beträgt somit1788 / 14 = 127
einschließlich des ersten Aufrufs (der kein rekursiver Aufruf ist).Wie Sie sehen, ist es sehr schwierig, aber nicht unmöglich, das zu finden, was Sie suchen.
Eine einfachere Möglichkeit, die zuvor verfügbare Stack-Größe abzurufen,
recurse()
ist die Verwendung der folgenden Funktion (im Adafruit Learning Center verfügbar ; ich habe sie nicht selbst getestet):Ich empfehle Ihnen nachdrücklich, diesen Artikel im Adafruit Learning Center zu lesen .
quelle
.bss
die globalen Variablen ohne Anfangswert in Ihrem Code dargestellt, wohingegendata
es sich um globale Variablen mit Anfangswert handelt. Letztendlich verwenden sie jedoch denselben Platz: Statische Daten im Diagramm.static
in einer Funktion deklariert sind.Rekursion ist eine schlechte Praxis auf einem Mikrocontroller, wie Sie bereits selbst angegeben haben und Sie möchten sie wahrscheinlich nach Möglichkeit vermeiden. Auf der Arduino-Website stehen einige Beispiele und Bibliotheken zum Überprüfen der freien RAM-Größe zur Verfügung . Sie können dies zum Beispiel verwenden, um herauszufinden, wann eine Rekursion zu unterbrechen ist, oder um etwas schwieriger / riskanter, Ihre Skizze zu profilieren und das darin enthaltene Limit festzuhalten. Dieses Profil wäre für jede Änderung in Ihrem Programm und für jede Änderung in der Arduino-Toolkette erforderlich.
quelle
Das hängt von der Funktion ab.
Jedes Mal, wenn eine Funktion aufgerufen wird, wird ein neuer Frame auf den Stapel gelegt. Es enthält normalerweise verschiedene kritische Elemente, die möglicherweise Folgendes umfassen:
this
), wenn eine Member-Funktion aufgerufen wird.Wie Sie sehen, hängt der für einen bestimmten Aufruf erforderliche Stapelspeicherplatz von der Funktion ab. Wenn Sie beispielsweise eine rekursive Funktion schreiben, die nur einen
int
Parameter verwendet und keine lokalen Variablen verwendet, benötigt sie nicht viel mehr als ein paar Bytes auf dem Stapel. Das heißt, Sie können es rekursiv weitaus mehr aufrufen als eine Funktion, die mehrere Parameter und viele lokale Variablen verwendet (was den Stapel viel schneller auffrisst).Offensichtlich hängt der Status des Stapels davon ab, was im Code noch vor sich geht. Wenn Sie eine Rekursion direkt in der Standardfunktion starten
loop()
, ist wahrscheinlich noch nicht viel auf dem Stapel. Wenn Sie es jedoch in anderen Funktionen mehrere Ebenen tief verschachtelt starten, ist weniger Platz vorhanden. Dies hat Einfluss darauf, wie oft Sie wiederkehren können, ohne den Stapel zu erschöpfen.Es ist erwähnenswert, dass es auf einigen Compilern eine Optimierung der Schwanzrekursion gibt (obwohl ich nicht sicher bin, ob avr-gcc dies unterstützt). Wenn der rekursive Aufruf das allerletzte Element in einer Funktion ist, ist es manchmal möglich, das Ändern des Stapelrahmens überhaupt zu vermeiden. Der Compiler kann den vorhandenen Frame einfach wiederverwenden, da der übergeordnete Aufruf (sozusagen) ihn nicht mehr verwendet. Das bedeutet, dass Sie theoretisch so oft rekursiv arbeiten können, wie Sie möchten, solange Ihre Funktion nichts anderes aufruft.
quelle
Ich hatte genau die gleiche Frage, als ich " Jumping into C ++" von Alex Allain , Kapitel 16: Rekursion, S. 230, las. Deshalb führte ich einige Tests durch.
TLDR;
Mein Arduino Nano (ATmega328 mcu) kann 211 rekursive Funktionsaufrufe ausführen (für den unten angegebenen Code), bevor er einen Stapelüberlauf hat und abstürzt.
Lassen Sie mich zunächst auf diese Behauptung eingehen:
[Update: ah, ich habe das Wort "schnell" überflogen. In diesem Fall haben Sie eine gewisse Gültigkeit. Trotzdem denke ich, dass es sich lohnt, Folgendes zu sagen.]
Nein, ich halte das nicht für eine wahre Aussage. Ich bin mir ziemlich sicher, dass alle Algorithmen ausnahmslos sowohl eine rekursive als auch eine nicht rekursive Lösung haben. Es ist nur manchmal deutlich einfachereinen rekursiven Algorithmus verwenden. Allerdings ist Rekursion für die Verwendung auf Mikrocontrollern sehr verpönt und würde in sicherheitskritischem Code wahrscheinlich niemals zugelassen werden. Trotzdem ist es natürlich möglich, dies auf Mikrocontrollern zu tun. Um zu wissen, wie "tief" Sie in eine bestimmte rekursive Funktion eintauchen können, testen Sie sie einfach! Führen Sie es in Ihrer realen Anwendung in einem realen Testfall aus, und entfernen Sie die Grundbedingung, damit es unendlich oft wiederkehrt. Drucken Sie einen Zähler aus und überzeugen Sie sich selbst, wie tief Sie gehen können, damit Sie wissen, ob Ihr rekursiver Algorithmus die Grenzen Ihres Arbeitsspeichers zu weit ausschöpft, um praktisch verwendet zu werden. Hier ist ein Beispiel, um einen Stapelüberlauf auf einem Arduino zu erzwingen.
Nun ein paar Anmerkungen:
Wie viele rekursive Aufrufe oder "Stapelrahmen" Sie erhalten können, hängt von einer Reihe von Faktoren ab, darunter:
free_RAM = total_RAM - stack_used - heap_used
oder Sie könnten sagenfree_RAM = stack_size_allocated - stack_size_used
)Meine Ergebnisse:
Segmentation fault (core dumped)
#pragma GCC optimize ("-O0")
Zum Anfang der Datei und Redo:Here are the final print results: 209 210 211 ⸮ 9⸮ 3⸮
Der Code:
Die PC-Anwendung:
Das Arduino "Sketch" -Programm:
Verweise:
#pragma GCC optimize
befehl, da ich wusste, dass ich es dort dokumentiert hatte.quelle
#pragma
Sie dort verwenden. Stattdessen können Sie__attribute__((optimize("O0")))
die einzelne Funktion, die Sie nicht optimieren möchten, ergänzen.Ich habe dieses einfache Testprogramm geschrieben:
Ich habe es für die Uno kompiliert und während ich schreibe, ist es über 1 Million Mal rekursiv vorgekommen! Ich weiß es nicht, aber der Compiler hat dieses Programm möglicherweise optimiert
quelle
call xxx
/ret
durch ersetztjmp xxx
. Dies entspricht dem gleichen Vorgang, mit der Ausnahme, dass die Compilermethode den Stack nicht verbraucht. Auf diese Weise können Sie Ihren Code milliardenfach wiederverwenden (andere Dinge sind gleich).#pragma GCC optimize ("-O0")
an die Spitze Ihres Arduino-Programms setzen. Ich glaube, Sie müssen dies am Anfang jeder Datei tun, auf die Sie sie anwenden möchten - aber ich habe das seit Jahren nicht mehr nachgeschlagen, also recherchieren Sie es selbst, um sicherzugehen.