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
voidmeine_super_funktion()
2
{
3
charmein_array[10000];
4
...
5
}
wird einen ATTiny auf jeden Fall erst zur Laufzeit zum Platzen bringen.
Das wird Dir nicht vorher angezeigt!
Bei:
1
voidmeine_super_funktion()
2
{
3
staticcharmein_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
voidfoo()
2
{
3
chartmp[20];
4
}
5
6
voidbar()
7
{
8
chartmp[80];
9
}
10
11
intmain()
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