Speichernutzung "dos and don'ts"

8

Obwohl ich schon lange C / C ++ - Code geschrieben habe, haben mich die unsichtbaren Einschränkungen des Speicherbedarfs auf verschiedenen MCU- und SOC-Programmierplattformen oft gestolpert. Da ich im Begriff bin, Code für mein erstes großes Projekt für meine NANO-Boards zu erstellen, das wahrscheinlich einen angemessenen Teil seiner Ressourcen verbraucht, möchte ich besser auf überraschende und unerwartete "Fallstricke" vorbereitet sein .

Zum Beispiel war ich bei einem anderen SOC, an dem ich kürzlich ausgiebig gearbeitet habe (The Pololu.com wixel), sehr überrascht, diese Funktions- / Methodenargumente und automatischen Variablen zu erfahren, von denen ich normalerweise erwarten würde, dass sie stapelweise zugewiesen und wiederhergestellt werden, wenn eine Funktion zurückgegeben wird in der Tat dauerhaft für die Laufzeit des Programms zugewiesen! Beeindruckend! Auf dieser Plattform, auf der ich es normalerweise hasse, Variablen zu "überarbeiten" oder Variablen wiederzuverwenden, nachdem ihre Namen keinen Sinn mehr ergeben, musste ich meine Codierung an das anpassen, was ich aus Gründen der Lesbarkeit normalerweise für BAD halte. Ganz zu schweigen von der Erkenntnis, dass einfache Schleifenvariablen besser global zugeordnet werden sollten. Ja!

Können diejenigen von Ihnen, die in der Arduino-Umgebung auf solche unerwarteten Codierungsprobleme gestoßen sind, einige "spezielle" Richtlinien zu solchen Dingen teilen?

Geil
quelle
1
autoin C ++ 11 von einem Speicherdauer-Bezeichner in einen Variablentyp-Bezeichner geändert. Der Einfachheit halber nicht verwenden auto.
Majenko
1
Eine positive Überraschung für mich ist die Array-Zuweisung auf Stapel mit Größe aus einer Variablen gcc.gnu.org/onlinedocs/gcc/Variable-Length.html
Juraj
Oh ... gee ... sorry ... ich wollte nie darauf schließen, dass ich jemals das Schlüsselwort "auto" verwendet habe (ich habe vergessen, dass es überhaupt existiert!). Ich meinte nur, was auch immer die Standardzuordnung für nicht statische Variablen war, die innerhalb einer Funktion deklariert wurden.
Randy

Antworten:

12

Wie Sie selbst bemerkt haben, kann dies etwas plattformabhängig sein. Da Sie an einem Nano arbeiten, ist meine Antwort für die AVR-Architektur, wenn sie mit gcc, avr-libc und der Arduino-Kernbibliothek kompiliert wird.

Lassen Sie mich zunächst beruhigen: Dieses seltsame Verhalten, das Sie beim Wixel gesehen haben, tritt auf dieser Plattform nicht auf. Lokale Variablen werden meistens in CPU-Registern und dann auf dem Stapel zugewiesen.

Nun mag die erste allgemeine Regel offensichtlich klingen: Überlegen Sie zweimal, woran sich Ihr Programm wirklich erinnern muss. Verwenden Sie außerdem jedes Mal den kleinstmöglichen Datentyp. Die C99 - Typ int8_t, uint16_tund co. sind dafür nützlich.

Eine andere Regel, die Sie auf dieser Plattform häufig hören werden: Vermeiden Sie dynamische Zuweisungen, dh malloc()und newSie können sich häufig keine Heap-Fragmentierung leisten. Vermeiden Sie auch die StringKlasse, da sie auf dynamischer Zuordnung beruht. Bevorzugen Sie nach Möglichkeit die statische Zuordnung oder die Stapelzuweisung für kleine Dinge.

Vergessen Sie nicht, constfür Konstanten zu verwenden: Mit diesem Qualifikationsmerkmal kann der Compiler die Konstanten häufig als unmittelbare Operanden optimieren. Verwenden Sie für Arrays von Konstanten PROGMEM. Ja, es ist nicht bequem zu bedienen, aber es kann Ihnen viel RAM sparen. Das F()Makro zum Drucken konstanter Nachrichten ist ein Sonderfall von PROGMEM.

Seien Sie zum Schluss sehr vorsichtig mit der Rekursion.

Edgar Bonet
quelle
Vielen Dank. Gute Argumente! Normalerweise vermeide ich Malloc oder "Neu" in einem eingebetteten Programm, insbesondere einem, das möglicherweise lange Zeit unbeaufsichtigt ausgeführt wird. Außerdem habe ich Codespace-Speicher für alle Strukturen oder Arrays verwendet, die sich nie ändern werden. Oder sogar EEprom-Strukturen für Dinge wie Einstellungen. Aber nur die Tatsache, dass Funktionsvariablen stapelbasiert sind, ist eine SEHR gute Nachricht. Ich begann mir Sorgen zu machen, so viel Beispielcode zu sehen, in dem immer wieder derselbe Variablenname verwendet wurde, auch wenn sein Name keinen Sinn mehr ergab.
Randy
Dieser letzte Punkt ist auch interessant! Ich frage mich, ob eine rekursive Funktion, die ihre Stapelgrenzen überschritten hat, das schlechte Board fast unerreichbar machen würde, da es sich jedes Mal, wenn es Strom sah, in dasselbe Loch stürzt. Es ist nicht wie bei den alten Geräten, die Sie mit einem Strom von STRG-Cs, die beim Einschalten gesendet werden, zum Stillstand bringen könnten!
Randy
1
@ Randy: Es gibt kein Stapellimit. Wenn Sie Ihren Stapel überlaufen, beginnen Sie einfach, den Heap des Programms zu überschreiben, falls vorhanden, dann den Abschnitt .bss und dann den Abschnitt .data.
Edgar Bonet
1
Es ist nicht richtig zu sagen, dass Dinge auf einem bestimmten Chip passieren oder nicht passieren, wenn sie tatsächlich Ergebnisse der Toolchain oder sogar der Sprachspezifikation sind und nicht der Hardware .
Chris Stratton
1
@ ChrisStratton: Du hast recht. Ich habe meiner Antwort eine Klarstellung hinzugefügt. Auf der anderen Seite wurde der AVR mit Blick auf die Kompilierung entwickelt. Es wäre albern (obwohl sicherlich möglich), wenn ein Compiler die Registerdatei und den Stapelzeiger nicht so verwendet, wie sie verwendet werden sollen.
Edgar Bonet
3

Edgar hat einige großartige Tipps, und ich werde einen wiederholen, bevor ich ein paar mehr gebe:

Vermeiden Sie dynamische Speicherzuweisungen

Verwenden Sie kein Malloc. Wenn Sie ein System entwerfen, budgetieren Sie Ihren RAM wie Ihr monatliches Budget. Stellen Sie sicher, dass Sie alles auf einmal erledigen können.

Und noch ein paar andere Tipps:

globale Statik

Nutzen Sie den BSS-Block voll aus. Versuchen Sie, den Speicherplatz in globalen staticArrays so weit wie möglich auszublenden. Sie mögen schaudern oder sich über die Idee lustig machen, alles auf eine globale Ebene zu stellen, aber eingebettet ist ein Paradigmenwechsel mit eigenen Regeln.

Da Sie auf globaler Ebene so viele Variablen haben, können staticSie vermeiden, dass Ihr Namespace zu stark verschmutzt wird.

Vermeiden Sie ausgefallene C ++ - Funktionen

Versuchen Sie in der Tat, C ++ überhaupt nicht zu verwenden. C ++ bringt Sie mit seinem RAII und seinen Konstruktoren und seiner impliziten Typkonvertierung "weiter vom Metall entfernt" ... Es ist viel schwieriger, alle Ihre Ressourcen genau zu erfassen, wenn Ihre Sprache hinter den Kulissen so viel tut. Wenn Sie wirklich eine bestimmte C ++ - Sprachfunktion verwenden möchten, versuchen Sie, den Ton Ihres Codes eher wie "C mit C ++ - Bits" als wie "C ++ in ein eingebettetes System eingebettet" beizubehalten. Das geht nie gut.

Vermeiden Sie insbesondere Generika / Vorlagen, die viel Speicherplatz beanspruchen. Jedes Mal, wenn Sie einen neuen Typ mit einer Vorlage in Ihrem Code verwenden, wird eine weitere Kopie der Vorlagenklasse in Ihr Programm kompiliert, nur für diesen einen Typ. Haben Sie sich jemals gefragt, warum C ++ - Binärdateien auf 20, 50 oder sogar 100 MB steigen können? Vorlagen, Mann. Vorlagen.

const

Dieses Schlüsselwort sollte so viele Variablen wie möglich enthalten. Wenn Sie diese Gewohnheit beibehalten, sparen Sie sowohl Ausführungszeit als auch Platzverbrauch. Lernen Sie den Unterschied zwischen const T* foo, T* const foound const T* const foo.

Eric Dand
quelle
1
Zu viel davon ist Unsinn, um es zu ignorieren. Einiges davon ist gut, aber die Empfehlung einer Reihe globaler Staaten ist nur ein schlechter Rat, und viele Leute verwenden C ++ für diese Art der Programmierung. Embedded ist keine Entschuldigung dafür, hart zu schreiben, um Code zu pflegen.
RubberDuck
3
Darüber hinaus müssen C ++ - Vorlagen nicht so schwer sein, wie Sie es sich vorstellen. Der Schlüssel besteht darin, sicherzustellen, dass Sie mit der Optimierung der Verknüpfungszeit kompilieren, um zu verhindern, dass jede einzelne Objektdatei eine eigene Kopie jeder verwendeten Vorlagenspezialisierung hat. 10 verschiedene Kopien von vector<int>jeweils einer von 10 Quelldateien werden etwas unhandlich, aber mit LTO kann der Compiler / Linker sie auf eine einzige gemeinsam genutzte Kopie deduplizieren. Wenn Sie die Funktionalität benötigen, ist der auf Vorlagen spezialisierte Code (insbesondere nach LTO-Inlining) häufig ziemlich ähnlich groß wie das, was Sie von Hand schreiben würden.
ShadowRanger
3
Machen Sie eine Variable nicht global, wenn sie nur in einer Funktion verwendet wird. Machen Sie es, staticwenn der Wert von Aufruf zu Aufruf gespeichert werden muss. In Bezug auf die Zuordnung ist ein statisches Lokal dasselbe wie ein globales.
Edgar Bonet
Danke für die Tipps. Ich sehe den Vorteil in einigen davon für kleine Systeme. Obwohl ich gesehen habe, dass C ++ - Compiler einen langen Weg zurückgelegt haben, zögere ich immer noch, "reines" C ++ zu verwenden und in jeder Ecke meines Codes 100% OOP zu sein. Manchmal ist es übertrieben. Wenn Sie jedoch gut etablierte Funktionen in Klassen integrieren, die Sie in einer Bibliothek verstauen können, ist es viel einfacher, Ihrer Anwendung zu folgen.
Randy