Forum: Compiler & IDEs Falsch gedacht... Stackframe


von Ralf R. (voltax)


Lesenswert?

(nur so als Info)

vielleicht ists allgemein bekannt, für mich wars jedenfalls neu: Ich 
nahm an, dass eine lokale Variable erst dann auf dem Stack landet, wenn 
sie aus "C"-Sicht bekannt ist. Ich stellte mir vor, dass der C-Compiler 
zB. an der "<-- da" den SP für das Array "dummy" decrementieren würde, 
um ihn an der Stelle "<-- dort" wieder zu incrementieren.

***das ist aber nicht so***


1
#include <avr/io.h>
2
3
static uint16_t debug;
4
5
void test ()
6
{
7
   debug = SP;
8
   
9
   {  // <-- da
10
      char dummy [23]; //--- Aus "C"-Sicht erst ab hier bekannt --- 
11
      debug = SP;      //--- Trotzdem schon von Anfang an auf dem Stack ---
12
      dummy [22] = 0;
13
      debug = SP;      
14
   } // <-- dort
15
16
   debug = SP;
17
}
18
19
int main()
20
{  
21
   debug = SP;   
22
   test (); 
23
   debug = SP;
24
   
25
   while (1==1);              
26
   return (0);
27
}

Grüße Ralf

von Peter D. (peda)


Lesenswert?

Hier ists doch egal.

Aber mach dochmal davor nen Unterprogrammaufruf in nem anderen Objekt.

Wenn diese Funktion auch Variablen anlegt, hätte man dadurch ne viel 
höhere unnötige Stackbelastung.


Peter

von Ralf R. (voltax)


Lesenswert?

>Hier ists doch egal.

das da oben sollte ja auch nur ein Beispiel sein.

Ich hatte mein Verständnisproblem in einer Parser-Funktion entdeckt, die 
sich selber rekursiv aufruft, und zwar entweder tief mit kleinen 
Buffern, oder weniger tief aber dann mit großen Buffern. Die Buffer 
hatte ich wie oben in dem Beispiel in eigenen Namensräumen deklariert, 
und war dann überrascht, dass trotzdem der Stack schnell überlief, - 
weil nämlich jedesmal sämtliche Buffer auf dem Stack reserviert wurden, 
und nich wie beabsichtigt nur die gerade benötigten. Aber kein Problem, 
ich löse das dann halt anders.

Grüße Ralf

von Peter D. (peda)


Lesenswert?

Rekursionen sind ne Erfindung von Mathematikern, um Algorithmen 
möglichst kompliziert und undurchschaubar darzustellen.


Ich kenne keine Architektur, die Rekursionen effektiv ausführen und 
keinen Compiler, der Rekursionen effektiv umstellen kann.

Schon allein der Call+Return+Stackvariablen-Overhead läßt eine ganz 
schwummrig werden, wenn man mal den generierten Assembler ansieht.

Besonders katastrophal sind Rekursionen, die sich 2-fach aufrufen (f(n), 
f(n-1)), dann steigt der Rechenzeitbedarf exponentiell.

Ich war völlig verduzt, als ein rekursiver Algorithmus plötzlich bei 
Rekursion 30 viele Sekunden auf nem 3GHz Pentium brauchte.

Auf nem 12MHz 8051 als Schleife aufgelöst, war die Zeit dagegen nicht 
mehr manuell meßbar (Enter drücken, Ergebnis stand sofort auf LCD).

Wenn man wirklich effektive CPU-Zeit und SRAM-Nutzung haben will, muß 
man selber die Rekursion in ne Schleife umformen.


Peter

von Roland Praml (Gast)


Lesenswert?

Um Rekursion zu verstehen muss man zuerst Rekursion verstehen :-)

bei µC's würde ich auch soweit auf Rekursion verzichten, falls man diese 
irgendwie aufdröseln kann.
Weiterhin meide ich persönlich Funktionen, wo man den Worst-Case der 
Speichernutzung nicht abschätzen kann

Gruß
Roland

von Peter D. (peda)


Lesenswert?

Roland Praml wrote:
> Um Rekursion zu verstehen muss man zuerst Rekursion verstehen :-)


Ich verstehe Rekursionen, sie sehen auf den ersten Blick auch schön kurz 
aus, allerdings wirklich nur auf den allerersten Blick.

Ich würde sogar soweit gehen Rekursionen für AKW- oder 
Raumfahrtsteuerungen usw. pauschal zu verbieten, weil sie eben 
unkalkulierbar sind.

Der Hauptnachteil ist die fehlende Abbruchbedingung bei unvermuteten 
Eingangsparametern.

Eine Schleife über n Felder kann dagegen bei n+1 sofort abbrechen und ne 
Fehlermeldung ausgeben ohne den Stack zu korrumpieren und alle anderen 
Prozesse damit zu killen.
Sie hat neben der wesentlich schnelleren Ausführung also noch den 
Vorteil der wesentlich höheren Sicherheit.


Ich hatte mal ein DIR /S auf ein Netzlaufwerk gegeben, UNIXer haben aber 
irgendwie die Angewohnheit, das Homeverzeichnis . nochmal als virtuelles 
Unterverzeichnis anzulegen.
Die DOS-Fehlermeldung nach einigen Stunden war wirklich seltsam.

Irgendwann hat man ihnen aber gesagt, daß rekursive Unterverzeichnisse 
Schwachsinn sind und dann konnte man wieder normal aufs Netz zugreifen.


Peter

von Ralf R. (voltax)


Lesenswert?

Hallo Peter,

ok, wechseln wir zum Thema "Rekursonen allgemein".

Und dazu meine ich dass man Rekursionen immer dann verwenden sollte, 
wenn es für die Lösung des Problems zweckmäßig ist. Rekursionen 
gegenüber linearer Programmierung als generell unsicherer zu bezeichnen, 
halte ich für falsch.

Durch geeignete Maßnahmen hat man beide Techniken im Programm gut im 
Griff, und es muß nicht notwendigerweise die Gefahr eines korrumpierten 
Stacks bestehen. Falls eine Rekursion (problembedingt) zum Aufbau eines 
tiefen Stacks neigt, dann würde üblicherweise auch die zu einem linaren 
Programm abgerollte Variante Daten zwischenspeichern müssen, zB. in 
einem Stack, welcher ebenfalls überlaufen kann.

Zur Frage der wesentlich schnelleren Ausführung eines linearen Programms 
gegenüber einer rekursiven Lösung meine ich, dass das möglicherweise, 
dann aber nur fast stimmt, denn ich glaube nicht, dass die Unterschiede 
"wesentlich" ausfallen würden, bestenfalls ist eine lineare Lösung 
geringfügig schneller als eine Rekursion. Wenn eine lineare Lösung 
deutlich schneller sein sollte, dann wäre eine rekursive Lösung für 
dieses Problem wahrscheinlich ohnehin die falsche Wahl gewesen.

Um mal einen schönen Angriffspunkt zu liefern (oder gleich mehrere), 
sage ich, worum es bei meiner Rekursion im Detail geht: Ich schreibe an 
einem Parser für arithmetische Ausdrücke (für meinen BASIC-Interpreter). 
Anhand der für BASIC üblichen Syntax soll er mathematische Berechnungen 
durchführen, und dazu einen String so Parsen, dass er Operatoren, 
Operanden und Klammern erkennt, und abhängig von deren Rangfolge korrekt 
berechnet (also Punkt vor Strichrechnung). Ich löse das mit einem 
rekursiven Algorithmus. Jetzt wäre die Frage offen, wie gut, wie schnell 
und wie übersichtlich sich das mit einem linearen Algorithmus lösen 
läßt, und ob er einer rekursiven Lösung in Sicherheitsaspekten überlegen 
wäre.

Grüße Ralf

von Andreas S. (andreas) (Admin) Benutzerseite


Lesenswert?

Peter Dannegger wrote:
> Ich hatte mal ein DIR /S auf ein Netzlaufwerk gegeben, UNIXer haben aber
> irgendwie die Angewohnheit, das Homeverzeichnis . nochmal als virtuelles
> Unterverzeichnis anzulegen.
> Die DOS-Fehlermeldung nach einigen Stunden war wirklich seltsam.
>
> Irgendwann hat man ihnen aber gesagt, daß rekursive Unterverzeichnisse
> Schwachsinn sind und dann konnte man wieder normal aufs Netz zugreifen.

. und .. sind virtuelle Verzeichnisse die vom System bereitgestellt 
werden, sowohl unter Windows als auch unter Unix. Dass dir /s in diese 
Verzeichnisse absteigt kann ich mir nicht vorstellen - dann würde es 
überhaupt nicht funktionieren.

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Rekursion ist weiter nichts als eine Variante dynamischer Ressourcen-
verwaltung, mit all ihren Vor- und Nachteilen.  Man kann damit z.B.
eben ein beliebig komplexes C-Programm zerlegen, wobei das "beliebig"
in der Praxis halt durch die verfügbaren Systemressourcen begrenzt
wird.  Da selbige bei einem AVR deutlich knapper sind als auf meinem
PC, wird man die Rekursion folglich auf diesem seltener antreffen.

Sinnvoll kann sie aber trotzdem sein, genauso wie auch malloc()
durchaus sinnvoll sein kann.

von Markus K. (markus-)


Lesenswert?

Peter Dannegger wrote:
> Ich hatte mal ein DIR /S auf ein Netzlaufwerk gegeben, UNIXer haben aber
> irgendwie die Angewohnheit, das Homeverzeichnis . nochmal als virtuelles
> Unterverzeichnis anzulegen.
> Die DOS-Fehlermeldung nach einigen Stunden war wirklich seltsam.
>
> Irgendwann hat man ihnen aber gesagt, daß rekursive Unterverzeichnisse
> Schwachsinn sind und dann konnte man wieder normal aufs Netz zugreifen.

Du bist hier vermutlich über einen Softlink gestolpert. Wenn der auf das 
aktuelle Verzeichnis zeigt, dann passiert genau sowas. Die Unix-Tools 
können damit natürlich umgehen, aber da es bei DOS das Konzept von 
virtuellen Unterverzeichnissen so nicht gibt, haben die beim 
Verzeichnis-Export das eben auf normale Unterverzeichnisse gemappt.

Markus

von Peter D. (peda)


Lesenswert?

Ralf Rosenkranz wrote:

> Um mal einen schönen Angriffspunkt zu liefern (oder gleich mehrere),
> sage ich, worum es bei meiner Rekursion im Detail geht: Ich schreibe an
> einem Parser für arithmetische Ausdrücke (für meinen BASIC-Interpreter).

Soll der Parser etwa auf nem AVR laufen ?
Dann machs doch so, wie in Bascom, also immer nur eine Berechnung pro 
Zeile erlauben.
Richtige Parser kenne ich eigentlich nur bei C-Compilern.


Peter

von Karl H. (kbuchegg)


Lesenswert?

Peter Dannegger wrote:
> Ralf Rosenkranz wrote:
>
>> Um mal einen schönen Angriffspunkt zu liefern (oder gleich mehrere),
>> sage ich, worum es bei meiner Rekursion im Detail geht: Ich schreibe an
>> einem Parser für arithmetische Ausdrücke (für meinen BASIC-Interpreter).
>
> Soll der Parser etwa auf nem AVR laufen ?
> Dann machs doch so, wie in Bascom, also immer nur eine Berechnung pro
> Zeile erlauben.
> Richtige Parser kenne ich eigentlich nur bei C-Compilern.

Jetzt untertreibst du aber. Auch der AVR-Assembler kann
arithmetische Ausdrücke mit Konstanten auflösen.

Ist auch weiter keine Hexerei. Man kann das mit eigenen
Stacks machen oder einen 'recursive decent parser' aufsetzen.
Ich muss gestehen, dass ich für Letzteres ein Faible habe,
auch wenn ich die mittlerweile nicht mehr selbst schreibe sondern
einen Compiler-Compiler darauf ansetze.

von Karl H. (kbuchegg)


Lesenswert?

> Wenn eine lineare Lösung deutlich schneller sein sollte,
> dann wäre eine rekursive Lösung für dieses Problem wahrscheinlich
> ohnehin die falsche Wahl gewesen.

Das Problem ist, dass Rekursion meistens mit 2 Beispielen
gelehrt wird:
* Fakultät einer Zahl
* Fibonacci Zahlen

Beide Problemstellungen kann man rekursiv lösen, jedoch
speziell bei Fibonacci Zahlen ist die naive rekursive
Variante eindeutig die schlechtest mögliche Variante,
wenn man auf Laufzeit aus ist. Fakultät ist auch nicht
anderes: Man kann es rekursiv machen, hat dann aber eine
schlechte Wahl getroffen.

Bei Listen bzw. Bäumen sind Rekursionen jedoch ein
natürliches Mittel um durch die Datenstruktur durchzugehen
(Bäume. Was Peter da beschrieb, ist kein Baum sondern ein
Netz. Das DOS Programm musste scheitern, nicht weil es rekursiv
verfasst wurde, sondern weil es nicht mit einem Netz gerechnet
hat. Das kann man aber kaum der Rekursion anlasten. Auch eine
lineare Lösung wäre kläglich gescheitert). OK, bei Listen sind
rekrusive Lösungen nicht unbedingt naheliegend und man wird
in der Regel keine rekursive Lösung wählen.

von Karl H. (kbuchegg)


Lesenswert?

> Ich hatte mein Verständnisproblem in einer Parser-Funktion entdeckt,
> die sich selber rekursiv aufruft, und zwar entweder tief mit
> kleinen Buffern, oder weniger tief aber dann mit großen Buffern.

Darf ich mal fragen, wie du den Parser aufbaust?
Ich hab schon viele Parser geschrieben, aber wenn ich mich
so zurückerinnere, fällt mir kein Beispiel ein, bei dem
es notwendig warm irgendwelche größeren Buffer anzulegen.
Die meisten Funktionen kommen mit 1, 2, 3
lokalen Variablen aus. Mehr ist eigentlich selten.
Vor allem bei arithmetischen Ausdrücken.

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

Btw., lex und yacc sollten sich auch auf dem AVR benutzen lassen.

von A.K. (Gast)


Lesenswert?

(f)lex und yacc/bison ersetzen Programmspeicher durch erheblichen 
Einsatz von Datenspeicher. Nicht gerade sinnvoll bei Microcontrollern 
mit Harvard-Architektur.

von Ralf R. (voltax)


Lesenswert?

@Karl heinz,

>> Ich hatte mein Verständnisproblem in einer Parser-Funktion entdeckt,
>> die sich selber rekursiv aufruft, und zwar entweder tief mit
>> kleinen Buffern, oder weniger tief aber dann mit großen Buffern.
>
> Darf ich mal fragen, wie du den Parser aufbaust?
> Ich hab schon viele Parser geschrieben, aber wenn ich mich
> so zurückerinnere, fällt mir kein Beispiel ein, bei dem
> es notwendig warm irgendwelche größeren Buffer anzulegen.
> Die meisten Funktionen kommen mit 1, 2, 3
> lokalen Variablen aus. Mehr ist eigentlich selten.
> Vor allem bei arithmetischen Ausdrücken.

öhm, ich habe nicht erwähnt, dass mein BASIC-Parser neben numerischen 
Werten auch an (bisher) einer Stelle kleine Strings verarbeinten kann. 
Ich behandele das Semikolon ";" wie einen Operator, welcher Srings 
zusammenfügt. (Später kommt noch das Komma dazu). Sobald der erste 
String-Operand feststeht (möglicherweise auch als Ergebnis einer 
Rekursion) und das Semikolon als Operator gefunden wurde, dann parse ich 
den zweiten Operanden rekursiv weiter. Vor den Eintritt in die Rekursion 
muß ich den ersten Operanden im Stack behalten, um ihn am Ende der 
Rekursion vor den zweiten Operanden hängen zu können. Das ist dann also 
dieser Fall, wo ich einen großen Buffer brauche.

Hier wäre ein Link zu meinem ursprübgichen Mini-BASIC-Interpreter, den 
ich mal in Java im Rahmen eines Javaquiz geschrieben hatte: 
http://basic.3dc.de
Der kann in etwa das, was die portierte avr-Version auch kann, dh. 
letztere hat noch ein paar zusätzliche Einschränkungen, und ist etwas 
unübersichtlicher als die Java-Version. Aber es reicht (wie schon 
geschrieben) bei beiden so gerade eben zum Apfelmännchenberechnen aus :)

Grüße Ralf

von Karl H. (kbuchegg)


Lesenswert?

> öhm, ich habe nicht erwähnt, dass mein BASIC-Parser neben
> numerischen Werten auch an (bisher) einer Stelle kleine Strings
> verarbeinten kann.

Ah, ok. Wenn Strings in Spiel kommen siehts natürlich anders
aus.

Gestern hats mich noch gepackt: Wieder mal einen Parser schreiben.
Ich hab mir dann noch schnell einen Rechner auf AVR-Basis
über UART gemacht. Nichts aufregendes: Grundrechenarten +-*/%
und Klammern, Punkt vor Strich. Die normale Expr-Term-Faktor
Grammatik halt. Ist zwar völlig sinnlos, aber hat Spass
gemacht :-)

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

A.K. wrote:

> (f)lex und yacc/bison ersetzen Programmspeicher durch erheblichen
> Einsatz von Datenspeicher.

Allerdings nur voller konstanter Tabellen, die müssten sich eigentlich
auch in den ROM auslagern lassen.  Hab ich aber zugegebenermaßen noch
nicht ernsthaft versucht.

Nein, sie "ersetzen" keinen Programmspeicher damit, sondern sie bauen
(wenn richtig benutzt) durchaus recht kompakten und schnellen Code,
indem sie eine state machine aufbauen.  Das ist normalerweise deutlich
effektiver als irgendwelche strcmp-Orgien, und wenn man Dinge wie das
Parsen von Strings mit aufnehmen will, hat man in irgendeiner Form
auch immer dynamischen Speicher mit dabei.

von Chris (Gast)


Lesenswert?

> Ich kenne keine Architektur, die Rekursionen effektiv ausführen
> und keinen Compiler, der Rekursionen effektiv umstellen kann.

Schau dir mal eine funktionale Sprache an, dann bekommst du eine andere 
Sichtweise zu sehen.
In Haskell zum Beispiel ist Iteration gar nicht möglich, weil diese 
Sprache "rein funktional" ist: Es gibt keine veränderbaren Variablen, 
nur Funktionen und deren Aufrufe. Parameter sind unveränderbar ("const") 
und Funktionsaufrufe sind referenziell transparent; das heißt, eine 
Funktion liefert immer exakt dasselbe Ergebnis, wenn man sie mit 
denselben Parametern aufruft, und ist absolut seiteneffektfrei. Das gilt 
für alle Funktionen, sogar für solche wie getLine, die statt eines 
Strings ein "IO-String-Objekt" liefern, das die Aktion "lies eine Zeile 
von stdin" beschreibt.

Mit ghc gibt es einen sehr guten Haskell-Compiler, der bei manchen 
Programmen sogar einem äquivalenten C-Programm das Wasser reichen kann.

Allerdings ist Haskell für Mikrocontroller wohl weniger geeignet, weil 
die benötigte Laufzeitbibliothek recht umfangreich ist.

von A.K. (Gast)


Lesenswert?

"Allerdings nur voller konstanter Tabellen, die müssten sich eigentlich
auch in den ROM auslagern lassen. "

Klar geht das. Weil aber GCC aber nicht mit Attributierung der 
entsprechenden Typen und Tabellen zufrieden ist, sondern pgm_read_... 
benötigt, muss man die Templates und evtl. den Codegenerator an beliebig 
vielen Stellen entsprechen anpassen. Das wird Arbeit. Wenn die Grammatik 
nicht zu komplex ist, ist man daher mit einem klassischen recursive 
decent parser besser dran.

Und das oben skizzierte Problem mit String-Variablen hat man so oder so. 
Auch yacc hat einen Stack, nur halt nicht für Rekursion sondern als 
Bestandteil seiner engine.

Nö, das ist einfach kein passender Job für AVRs. Eher schon für MSP430 
oder natürlich ARMs.

von A.K. (Gast)


Lesenswert?

Mal zurück zur Rekursion selbst. Klar lässt sich jedes rekursive Problem 
auch iterativ erledigen. Neben Effizienz des laufenden Codes gibt es 
allerdings auch eine Effizienz bei der Programmierung und insbesondere 
bei der Pflege des Codes. Und da kann Rekursion weit lesbarer sein als 
dessen krampfige Vermeidung.

von Ralf R. (voltax)


Lesenswert?

@Karl heinz Buchegger

> Ich hab mir dann noch schnell einen Rechner auf AVR-Basis
> über UART gemacht ...

oh interessant. Wäre mal spannend, wie ein solches Gerät vom C-Profi 
geschrieben innen aussieht. Ich komme ja mehr aus der Java-Ecke und 
schreibe zudem nicht so sehr kompakten Code, und so sieht mein Parser 
dann auch aus. Wir können ja einen neuen Thread starten: "Die schönsten, 
die fettesten, die schnellsten, die umständlichsten, und alle anderen 
UART-Rechner-Implementierungen auf dem ATmega8" oder so.

> ... Ist zwar völlig sinnlos, aber hat Spass gemacht :-)

gerade die sinnlosen Dinge machen ja oft den größten Spaß :)

Grüße Ralf

von Karl H. (kbuchegg)


Lesenswert?

Ralf Rosenkranz wrote:
> @Karl heinz Buchegger
>
>> Ich hab mir dann noch schnell einen Rechner auf AVR-Basis
>> über UART gemacht ...
>
> oh interessant. Wäre mal spannend, wie ein solches Gerät vom C-Profi
> geschrieben innen aussieht.

Ich muss dich aber warnen. Das war 1 Uhr nachts.
Wenn ich zu Hause bin, poste ich das mal.

von Karl H. (kbuchegg)


Angehängte Dateien:

Lesenswert?

So. Da ist das Machwerk.
Wie gesagt: Ist schnell zusammengeschustert.

Es implementiert die übliche Grammatik

Expression := Term | { [ '+' | '-' ] Term }.
Term := Faktor { [ '*' | '/' | '%' ] Faktor }.
Faktor := [ '-' ] ( Number | ( '(' Expression ')' ) ).

Für einen lexikalischen Parser war ich zu faul. Das
Zahlenparsen hab ich daher in die Grammatik verlagert.

Number := Digit { Digit }.

von Frank (Gast)


Lesenswert?

@Karl Heinz Buchegger,

danke! Schönes Stück Code zum lernen und besserwerden.

Frank:-)

von Ralf R. (voltax)


Angehängte Dateien:

Lesenswert?

@Karl heinz Buchegger

alle Achtung, da scheint kein Gramm überflüssiger Code in Deinem Parser 
zu sein, genial gelöst finde ich. Schlank und präzise.

na der Vollständigkeit halber habe ich auch meinene Parserbasteleien mal 
in der angehängten Datei zusammengefasst (und etwas zusammengekürzt), 
alles darin ist zugegebenermaßen etwas umständlicher als in Deinem Code. 
Und auch die Parserstrategie ist anders, nicht so konsequent rekursiv 
wie bei Dir. Aber man kann trotzdem rechnen damit, +-*/ und Klammern. 
Die Ein- und Ausgabe erfolgt ebenfalls über UART mit 9600 Baud,8,n,1

Grüße Ralf

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
Noch kein Account? Hier anmelden.