Hallo zusammen, ich habe nach dem Kompilieren mit der GNU Toolchain eine fertige ELF-Datei. - Wie kann ich jetzt den tatsächlichen Speicherbedarf herausfinden? Ist das "size"-Tool zum Anzeigen von "text", "data" und "bss" dafür ausreichend? - Gibt es irgendwelche "Sonderfälle" bei denen ich falsche Ergebnisse bekomme? - Wie ist der Zusammenhang zum Linker-Script? Das dürfte ja keinen Einfluss auf den tatsächlichen Speicherbedarf haben. Gruß Dennis
Das size Tool zeigt leider nicht an, wieviel dynamischen Speicher dein Programm belegt. Das kannst du nur zur Laufzeit ungefähr herausfinden. Zum Beispiel so: Belege mit malloc wiederholt 128 Bytes RAM, bis es nicht mehr geht. Die Summe davon ist der (ungefähr) verfügbare Speicher. Der Haken ist: Es kann sein, dass kein 128 Byte Block am Stück mehr verfügbar ist, aber noch jede Menge kleinere Blöcke. Andererseits macht es keinen Sinn, 1-byte Blöcke anzutesten, weil dann die Verwaltungsdaten dazu mehr Platz beanspruchen, als die Nutzdaten.
Dennis S. schrieb: > - Wie kann ich jetzt den tatsächlichen Speicherbedarf herausfinden? Ist > das "size"-Tool zum Anzeigen von "text", "data" und "bss" dafür > ausreichend? Es ist - soweit ich weiß - die einzige Möglichkeit. Hier wird aber nur der statische Speicherplatzberbrauch angezeigt. > - Gibt es irgendwelche "Sonderfälle" bei denen ich falsche Ergebnisse > bekomme? Der Speicherplatz von lokalen Variablen auf dem Stack kann nicht berechnet werden, da dies ja vom Laufzeitverhalten des Programms abhängt. Ein:
1 | void meine_super_funktion () |
2 | {
|
3 | char mein_array[10000]; |
4 | ...
|
5 | }
|
wird einen ATTiny auf jeden Fall erst zur Laufzeit zum Platzen bringen. Das wird Dir nicht vorher angezeigt! Bei:
1 | void meine_super_funktion () |
2 | {
|
3 | static char mein_array[10000]; |
4 | ...
|
5 | }
|
jedoch schon. Aber nun hast Du eventuell auch das Verhalten Deiner Funktion geändert....
@Stefan us Interessanter Ansatz, danke dafür! @Frank M. Okay, aber beim Flash-Verbrauch sollte ich doch mit dem Tool auf der sicheren Seite sein oder? Mir ist nicht ganz klar, warum die lokalen Variablen nicht berücksichtigt werden. Ist das darin begründet, dass die entsprechende Funktion unter Umständen gar nicht ausgeführt wird? Mir ist klar, dass bei jedem Funktionsaufruf die Variable an einer anderen Stelle liegen kann, aber dass sie in einer bestimmten Größe existieren muss, ist doch sicher? Gruß Dennis Edit: Hmm... bei rekursiven Aufrufen wäre die Variable natürlich öfter vorhanden...
Dennis S. schrieb: > Mir ist nicht ganz klar, warum die lokalen Variablen nicht > berücksichtigt werden. Ist das darin begründet, dass die entsprechende > Funktion unter Umständen gar nicht ausgeführt wird? > Mir ist klar, dass bei jedem Funktionsaufruf die Variable an einer > anderen Stelle liegen kann, aber dass sie in einer bestimmten Größe > existieren muss, ist doch sicher?
1 | void foo() |
2 | {
|
3 | char tmp[20]; |
4 | }
|
5 | |
6 | void bar() |
7 | {
|
8 | char tmp[80]; |
9 | }
|
10 | |
11 | int main() |
12 | {
|
13 | if( PINC & ( 1 << 0 ) |
14 | foo(); |
15 | else
|
16 | bar(); |
17 | }
|
woher soll der Compiler wissen, ob der Pin auf 0 ist oder nicht?
Dennis S. schrieb: > Edit: Hmm... bei rekursiven Aufrufen wäre die Variable natürlich öfter > vorhanden... Eben, der Speicher wird bei lokalen Variablen dynamisch verwendet. In der Regel wird dafür ein Stack benutzt, auf dem sich die lokalen Variablen stapeln. Beim Funktionsaufruf kommen neue Variablen auf den Stapel, beim Return werden sie wieder vom Stapel genommen. Genauer gesagt, ändert sich nur der Stackpointer nach oben und nach unten. Aus diesem Grunde ist der Speicherinhalt einer lokalen Variablen auch anfangs nicht definiert. Da kann alles mögliche drin stehen. Deshalb sollte man lokale Variablen tunlichst auch initialisieren, bevor man auf sie lesend zugreift.
Karl Heinz schrieb: > woher soll der Compiler wissen, ob der Pin auf 0 ist oder nicht? Das ist das eine. Das andere ist, dass Funktionen ja je nach Programmlogik in verschiedenen Hierarchieebenen aufgerufen werden können. Ein entsprechendes Tool müsste also hergehen und im Grunde den kompletten Call-Graphen aufbauen um festzustellen, welches der maximale Speicherverbrauch im Worst Case ist. Egal ob der tatsächlich eintritt oder nicht, bzw. ob der überhaupt bestimmbar ist oder nicht (Rekursion). Weiters gibt es dann noch die Komplikation von Interrupt Routinen. Je nachdem zu welchem Zeitpunkt der Interrupt auftritt oder nicht, hast du eine andere Grundlast am Stack, zu der sich dann noch der Speicherverbrauch der ISR samt der darin aufgerufenen Funktionen addiert. Das kann sich allerdings wieder ändern, wenn zwischendurch im Hauptprogramm die Interrupts gesperrt sind bzw. freigegeben werden. D.h. in so eine Fall müsste man dann tatsächlich die Programmausführung simulieren, mit allen nur denkbaren Möglichkeiten inklusive mehrerer vorhandener ISR die sich im schlimmsten Programmierfall auch noch gegenseitig unterbrechen könnten, um eine realistische aussagefähige Zahl zu bekommen. All das macht aber avr-size nicht.
Karl Heinz schrieb: > woher soll der Compiler wissen, ob der Pin auf 0 ist oder nicht? Naja, schon klar! Allerdings impliziert das dann auch, dass der Compiler IMMER solche Dinge wegoptimiert, unabhängig von der Einstellung -O?
Dennis S. schrieb: > Karl Heinz schrieb: >> woher soll der Compiler wissen, ob der Pin auf 0 ist oder nicht? > > Naja, schon klar! Allerdings impliziert das dann auch, dass der Compiler > IMMER solche Dinge wegoptimiert, unabhängig von der Einstellung -O? Genau aus diesem Grund hab ich einen Portpin für das Beispiel genommen. Denn über dessen Wert weiss der Compiler nichts. Was aber nicht bedeuten muss, dass der Pin jemals eine 1 liefert. Du könntest ja den Pin auch extern auf GND verdrahtet haben.
Hi, sollte das Entwicklungssystem unserer Wahl keine entsprechenden Werkzeuge an Bord haben, kann man z.B. folgenden Ansatz wählen: Nachdem uns der Linker den statischen Speicherbedarf gemeldet hat, können wir zur Laufzeit den dynamischen Speicherbedarf ermitteln. Vom Linkerskript wissen wir, wo Stack und Heap im RAM liegen. Z.B. auf dem ARM Cortex des STM32 läuft der Heap von unten hoch, der Stack kommt von oben runter. Im Startup-Code, weit vor der Main und noch bevor der Compiler seine Initialisierung macht, initialisieren wir den Stackbereich mit einem Marker (z.B. $A5) und den Heap mit einem anderen Marker (z.B. $AF). In der main-Loop durchforsten wir nun regelmäßig den Heap von TOP nach BOTTOM. Die erste Speicherstelle, die nicht mehr den Marker enthält zeigt uns den maximal verwendeten Heap-Speicher an. Für den Stack machen wir dasselbe umgedreht - von BOTTOM in Richtung TOP. Damit erfahren wir, wie tief der Stack jemals belegt war. Die damit gewonnenen Erkenntnisse kann man nun z.B. für die Statistik anzeigen. Und, wenn bestimmte Limits überschritten werden, wird ein Desaster-Recovery angestoßen: - während der Entwicklung eine Ausgabe (bevorzugt ohne calls ;) ) mit anschließender for-ever-Schleife - in der Produktionsversion eine angemessene Reaktion (dieser Punkt erfordert wirklich Fingerspitzengefühl) Grüße, Marcus
Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.