Hallo,
Programmiere einen Tiny261 mit GCC. Wie tief darf die
Verschachtelungstiefe durch den Aufruf von Unterprogrammen eigentlich
sein und wie seh' ich, falls es dadurch ein Problem gibt?
Danke! Martin
Ein paar Punkte, die Dir dabei helfen könn(t)en, es selbst
herauszufinden:
- Wie groß ist der für den Stack verfügbare Speicher?
- Wieviel Stack wird für jeden "Unterprogramm"-Aufruf verbraucht?
- Wieviel Stack belegen "Unterprogramme", wenn sie automatische
Variablen verwenden?
Hallo, danke für die Antwort.
Dann muss ich also ausprobieren, d.h. das Programm im Simulator laufen
lassen und den Stack beobachten? Gibt es keine andere Möglichkeiten;
könnte dies nicht der Compiler checken o.ä.?
Danke! Martin
Martin Lutz wrote:
> Hallo, danke für die Antwort.> Dann muss ich also ausprobieren, d.h. das Programm im Simulator laufen> lassen und den Stack beobachten?
Ja, ist eine Möglichkeit
> Gibt es keine andere Möglichkeiten;
Man könnte beim Programmstart den Speicher mal mit einem bestimmten
Byte fluten und nach einiger Zeit nachsehen, wieviel von diesem
'Muster' noch im Speicher vorhanden ist. Dort wo das Muster noch
existiert, ist der Speicher offenbar nicht beschrieben worden
und daher hat sich der Stack auch nicht bis dorthin ausgedehnt.
> könnte dies nicht der Compiler checken o.ä.?
Wie soll er das tun?
In dem Moment in dem ein paar Funktionsaufrufe von irgendwelchen
Bedingungen abhängen und diese wiederrum von irgendwelchen
Zuständen irgendwelcher I/O Pins, hängt der Speicherverbrauch
davon ab, was sich extern an den Anschlüssen tut. Nur: Wie
soll der Compiler wissen, was sich extern an den Anschlüssen
tut?
Das einzige was der Compiler machen kann: Bei jedem Funktions-
aufruf am Anfang der Funktion einen Check einbauen, so dass
die Funktion selbst prüft, ob der Stackpointer in Kollision
mit dem Heap kommt. Der Nachteil: Das verbaucht Resourcen,
sowohl Laufzeit als auch Programm-Speicher. Beides ist
auf einem µC sowieso meistens knapp.
Verschachtelte Aufrufe KANN der Compiler nicht prüfen, da dies ja kein
sttischer Wert ist sondern dynamisch - währent der Ausführung -
passiert. Ein Compiler kann nicht in die Kristallkugel schauen, wie oft
sich ein verschaltelter Aufruf verschachtelt. Also selbst ist der µC
Programierer, und sehen, wie viel Stack brauchst Du pro Aufruf, wieviele
lokale Variablen benötigst Du dabei. Dem ganzen ziehst Du den Platz der
globalen Variablen ab (die liegen an den unteren Adressen des RAM).
Daraus kannst Du Dir die benötigte Speichergrösse ableiten. Achtung, am
besten siehst Du Dir des ASM-Code an, der Controller pusht auch gerne
Register auf den Stack, bevor er die Subroutine ausführt. Jedenfalls
kommt die Returnadresse auf den Stack.
1
avr-objdump -g myProject.elf
liefert sehr brauchbare Angaben.
Viel Spass beim Zählen ist eine eher langweilige Aufgabe. Kann der
Simulator den Du verwendest Stackframes und Variablen im Speicher
anzeigen?
Gabriel Wegscheider wrote:
> Verschachtelte Aufrufe KANN der Compiler nicht prüfen, da dies ja kein> sttischer Wert ist sondern dynamisch - währent der Ausführung -> passiert. Ein Compiler kann nicht in die Kristallkugel schauen, wie oft> sich ein verschaltelter Aufruf verschachtelt.
Das könnte der Compiler sehr wohl tun, wenn man davon ausgeht, daß alle
Bedingungen wahr sind und keine Rekursion erfolgt.
Der Keil C51 macht das auch, um den SRAM überlagern zu können.
Rekursionen sollte man auf nem MC generell meiden, da sie exzessiven
SRAM- und CPU-Zeit-Verbrauch bewirken.
Ich schreibe gerne viele kleine Funktionen (ab 5-Zeiler) und hatte noch
nie Probleme wegen der Schachtelungstiefe.
Vielleicht beim ATtiny13 könnte es knapp werden (64 Byte SRAM), aber da
geht eher der Flash aus (512 Words).
Peter
Irgendwo hier flog mal ein Stück Code rum, das den Stackverbrauch
"nachmessen" kann:
in einer der .init-Sections wird der Speicher mit nem Wert
überschrieben, und später kann man nachzählen wieviel vom Speicher noch
diesen Wert hat, also wie tief der Stack in der Zwischenzeit gewachsen
ist.
mem-check.h:
Bei einem so kleinen Controller würde ich keine "lokalen" Variablen
empfehlen. Dann gibt einem der Compiler gleich eine Übersicht, über
den verbrauch an RAM (glaub .bss ?). Ausserdem meckert er in jedem Fall,
wenn der Speicher ausgeht.
Das kann man dann nutzen, um die Größe des restlichen RAMs zu ermitteln:
Array anlegen und die Größe solange erhöhen bis eine Fehlermeldung
kommt.
Die Größe, bevor die Fehlermeldung kommt, ist dann der noch freie
Speicher.
Dann hängt es letztlich von der Anzahl der Funktionen ab, die
verschachtelt aufgerufen werden.
Ein AVR braucht 2 bzw. 3 Byte pro Funktionsaufruf, um die
Rückspringadresse zu sichern. Der Compiler wird dann wahrscheinlich noch
ein paar Register sichern, die innerhalb der Funktion benutzt werden.
Hat man nur globale Variablen, sollte das hoffentlich recht wenig sein.
Kann man allerdings im Assemblercode prüfen.
Bei kleinen Controllern kann man sich die Verschachtelung meistens an
einer Hand abzählen. Dazu rechnet man dann noch einen ISR Aufruf dazu.
Weiss jeman, wie man die Anzahl der vom Compiler gesicherten Register
bei einem Funktionsaufruf abschätzen / bestimmen kann? (ausser im
Assemblercode zu schauen)?
Peter Dannegger wrote:
>Das könnte der Compiler sehr wohl tun, wenn man davon ausgeht, daß alle>Bedingungen wahr sind und keine Rekursion erfolgt.>>Der Keil C51 macht das auch, um den SRAM überlagern zu können.>>Rekursionen sollte man auf nem MC generell meiden, da sie exzessiven>SRAM- und CPU-Zeit-Verbrauch bewirken.
Das ist aber eigentlich nicht die Aufgabe des Compilers, schön wenn
dieser Compiler das kann.
Matthias wrote:
> Bei einem so kleinen Controller würde ich keine "lokalen" Variablen> empfehlen. Dann gibt einem der Compiler gleich eine Übersicht, über> den verbrauch an RAM (glaub .bss ?). Ausserdem meckert er in jedem Fall,> wenn der Speicher ausgeht.
Gerade bei einem kleinen Controller sind lokale Variblan sinnvoll, das
hier der Speicher mehrfach genutzt wird. Die lokalen Variablen werden
auf dem Stack angelegt und der verwendete Speicher ist dannach wieder
frei. Globale Variablen hingegen belegen den Speicher von Anfang an und
schliessen die Möglichkeit aus, einen Speicherbereich merhrfach zu
verwenden.
> Das kann man dann nutzen, um die Größe des restlichen RAMs zu ermitteln:>> Array anlegen und die Größe solange erhöhen bis eine Fehlermeldung> kommt.> Die Größe, bevor die Fehlermeldung kommt, ist dann der noch freie> Speicher.>> Dann hängt es letztlich von der Anzahl der Funktionen ab, die> verschachtelt aufgerufen werden.> Ein AVR braucht 2 bzw. 3 Byte pro Funktionsaufruf, um die> Rückspringadresse zu sichern. Der Compiler wird dann wahrscheinlich noch> ein paar Register sichern, die innerhalb der Funktion benutzt werden.>
Und mit den globalen Variablen hast Du noch lange nicht den
Speicherverbrauch abgedecht. Bei jedem Funktionsaufruf werden die
Parameter, die Rücksprungadresse und "ganz nach Lust und Laune" des
Compilers Register abgelegt (push)
> Hat man nur globale Variablen, sollte das hoffentlich recht wenig sein.> Kann man allerdings im Assemblercode prüfen.>> Bei kleinen Controllern kann man sich die Verschachtelung meistens an> einer Hand abzählen. Dazu rechnet man dann noch einen ISR Aufruf dazu.>> Weiss jeman, wie man die Anzahl der vom Compiler gesicherten Register> bei einem Funktionsaufruf abschätzen / bestimmen kann? (ausser im> Assemblercode zu schauen)?
Nein kann man nicht einheitlich sagen. Ich hab in einem kleine Projekt
gerade Stackframes-Sizes zwischen 4 und 18 Bytes gemessen.
Die Grösse der Stackframes ist sehr unterschiedlich, lässt jedoch für
jede Funktion auch ausrechnen.
1
avr-objdump -g myProject.elf
liefert alle Daten, "ein paar regexp mit etwas logik" darüberlaufen und
man kann einen Report generieren:
1
StackFrame: 'global' from 0xFFFFFFFF to 0x0 PushCount 0 FrameSize 8388719
2
Var 'PID_Param.Kp ' of 'int16_t ' @ 0x800068
3
Var 'PID_Param.Ki ' of 'int16_t ' @ 0x80006A
4
Var 'PID_Param.Kd ' of 'int16_t ' @ 0x80006C
5
Var 'PID_Param.Ta ' of 'int16_t ' @ 0x80006E
6
Var 'DEBUG ' of 'int16_t ' @ 0x800064
7
Var 'uDEBUG ' of 'uint16_t ' @ 0x800066
8
Var 'sum ' of 'int16_t ' @ 0x800062
9
Var 'last_error ' of 'int16_t ' @ 0x800060
10
StackFrame: '__vectors' from 0x0 to 0x26 PushCount 0 FrameSize 0
11
StackFrame: '__bad_interrupt' from 0x5A to 0x5C PushCount 0 FrameSize 0
12
StackFrame: 'main' from 0x5C to 0xB8 PushCount 0 FrameSize 8
13
Var 'result ' of 'unsigned_int ' @ 0x3
14
Var 'ref ' of 'unsigned_int ' @ 0x5
15
Var 'sens ' of 'unsigned_int ' @ 0x7
16
Var 'x ' of 'unsigned_int ' @ 0x1
17
StackFrame: 'PIDcontroller_init' from 0xB8 to 0x102 PushCount 2 FrameSize 4
18
Var 'reference ' of 'int16_t ' @ 0x1
19
Var 'process ' of 'int16_t ' @ 0x3
20
StackFrame: 'multULimit' from 0x102 to 0x164 PushCount 2 FrameSize 6
21
Var 'a ' of 'uint16_t ' @ 0x3
22
Var 'b ' of 'uint16_t ' @ 0x5
23
Var 'result ' of 'uint16_t ' @ 0x1
24
StackFrame: 'multSULimit' from 0x164 to 0x244 PushCount 6 FrameSize 8
25
Var 'a ' of 'int16_t ' @ 0x5
26
Var 'b ' of 'uint16_t ' @ 0x7
27
Var 'result ' of 'int32_t ' @ 0x1
28
StackFrame: 'PIDcontrollerUnsigned_schedule' from 0x244 to 0x412 PushCount 2 FrameSize 18
Sobald dann inline asm drinnen ist, macht mein Analyscode derzeit eine
Bauchlandung.... Da liefert nähmlich avr-objdump keine sinnvollen
Ergebnisse mehr :(
Matthias wrote:
> Bei einem so kleinen Controller würde ich keine "lokalen" Variablen> empfehlen.
Im Gegenteil. Ein AVR hat recht viele Register und die meisten
Funktionen kommen deshalb ausschliesslich mit Registern aus, müssen also
dafür keinen Speicher adressieren. Das kommt dem Code sehr zugute.
Was hingegen tatsählich sinnvoll sein kann: nicht-skalare Variablen
(arrays, structs) und Variablen deren Adresse verwendet wird als static
zu deklarieren. Denn solche Variablen landen sonst unweigerlich auf dem
Stack, und AVR kann damit zwar besser umgehen als etliche anderen
8-Bitter, aber immer noch schlechter als mit direkt adressierten.
Ausserdem ist die Verwaltung eines Stackframe beim GCC u.U. recht
aufwendig, wird aber nur nötig wenn solche Variablen überhaupt
existieren.
> Dann gibt einem der Compiler gleich eine Übersicht,
Inwieweit das ein Vorteil ist hängt auch vom Compiler ab. Auf
Microcontroller wie 8051 und PIC spezialisierte Compiler pflegen das
Program draufhin zu untersuchen, welche Variablen übereinander gelegt
werden können. GCC tut das aufgrund völlig anderer Herkunft nicht. Daher
sorgen statisch angelegte Variablen beim GCC für deutlich höheren
RAM-Verbrauch als lokale Variablen.
Zudem leidet der Code massiv, wenn skalare Daten statisch angelegt
werden, so dass nebem dem RAM auch noch das ROM knapp werden kann.
Gabriel Wegscheider wrote:
> liefert alle Daten, "ein paar regexp mit etwas logik" darüberlaufen und> man kann einen Report generieren:
Wobei man da ein bischen was draufrechnen muss. Seht dort 0, kommen noch
2-3 Bytes für die Return-Adresse dazu. Bei > 0 muss man darüber hinaus 2
Bytes für den Frame-Pointer hinzurechnen.
Andreas Kaiser wrote:
>> liefert alle Daten, "ein paar regexp mit etwas logik" darüberlaufen und>> man kann einen Report generieren:> Wobei man da ein bischen was draufrechnen muss. Seht dort 0, kommen noch> 2-3 Bytes für die Return-Adresse dazu. Bei > 0 muss man darüber hinaus 2> Bytes für den Frame-Pointer hinzurechnen.
Hier nicht!
Es steht 0 dort, wenn der Stackframe (oder pseudo-Stackframe) 0 ist. die
Sektion __vectors ist die Jumptable am Anfang des Programmspeichers, die
hat KEINEN Frame, daher 0 Byte Framesize. "global" ist auch kein echter
Frame, hab ihm aber in der Datenstruktur Frames reingepackt. Daher hat
"global" auch eine Size von -1.
Die Returnadresse, Returnparameter und Funktiuonsparameter hab ich bei
diesen Berechnungen bereits berücksichtigt. Der PushCount wird aus
"avr-objdump -d" extrahiert.
Soweit ich das im Simulator verifiziert habe stimmen diese Frameangaben
(sonst hätt ich sie ja korrigiert... :) )
... das mit "ein paar regexp und etwas logik" wahr etwas salopp
formuliert, das Parsen der avr-objdumps entspricht ~ 300 LOC (CPP mit
vielen RegExp). Die Strukturen noch nicht eingerechnet.
@Peter:
Da gefällt mir die Version die ich weiter oben gepostet hab aber besser.
Die kann man einfach irgendwo, z.B. in der main-schleife, aufrufen und
das Ergebnis ausgeben, und hat dort den Peak-Wert des Stackverbrauchs,
inkl. IRQs die seit Programmstart aufgerufen wurden.
Ernst Bachmann wrote:
> @Peter:>> Da gefällt mir die Version die ich weiter oben gepostet hab aber besser.> Die kann man einfach irgendwo, z.B. in der main-schleife, aufrufen und> das Ergebnis ausgeben, und hat dort den Peak-Wert des Stackverbrauchs,> inkl. IRQs die seit Programmstart aufgerufen wurden.
Ich wüßte jetzt nicht, wo meine Funktion Beschränkungen bezüglich der
Aufrufbarkeit hätte.
Man ruft sie einmal am Anfang des main auf, damit sie den ungenutzten
Bereich mit dem Muster füllt.
Und dann an beliebiger anderer Stelle, um den Verbrauch, seit dem
letzten Aufruf zu ermitteln.
Das ist ganz praktisch. Man kann damit eine Sequenz klammern, die man
als Speicherfresser in Verdacht hat und prüfen.
Man kann sie also beliebig und mehrmals aufrufen.
Ich versuche möglichst kein Assembler zu nehmen, wenns auch in C geht.
Peter
@Peter:
Ups, stimmt...
Hab ganz übersehen das die Schleife sozusagen den freien Speicher für
den nächsten Aufruf vorbereitet, und bin wg. dem Schleifenende bei "SP"
davon ausgegangen dass er nur den aktuellen Stack-Füllstand
betrachtet...
Nehme also alles zurück und behaupte das Gegenteil, schöne Funktion und
spart das Herumgefummel in den .init-Sections.