Forum: Mikrocontroller und Digitale Elektronik Cortex-M: Stack Begriffserklärung bzw. Handhabung


von Ralf (Gast)


Lesenswert?

Hi,

ich versteh den Begriff "Stack" in Zusammenhang mit den Cortex-M 
Controllern offenbar noch nicht und ich hab's über die SuFu nicht 
rausgefunden.

Für einen 8051 ist der Stack nichts weiter als ein Speicher, auf dem 
Rücksprungadressen und Registerinhalte gesichert werden, beispielsweise 
beim Funktionsaufruf und in Interrupt-Service-Routinen.

Der Stack im Cortex erfüllt die gleiche Funktion, im Forum hab ich aber 
auch gelesen, dass lokale Variablen von Funktionen ebenfalls auch auf 
dem Stack abgelegt werden (ich nehme mal an, dass das nicht GCC 
spezifisch ist, deswegen poste ich es nicht unter GCC), und globale 
Variablen ganz normal im SRAM. Dynamisches Speichermanagement verwendet 
den Heap, welcher für nichts anderes verwendet wird. Ist das soweit 
korrekt?

Wie funktioniert denn jetzt das mit den lokalen Variablen, wenn die auf 
dem Stack abgebildet werden? Auf einem Stack sollen doch Werte gesichert 
werden, d.h. die verändern sich nicht bzw. sollen es nicht.

Oder kann es sein, dass mit "Stack" gar nicht der Hardware-Stack gemeint 
ist, sondern ein dedizierter, vom Linker in der Größe berechneter 
Speicherbereich?

Ralf

von Tilo (Gast)


Lesenswert?

Der Stack liegt im S-Ram. Es gibt keine HW-Stack.

Was meinst du mit "lokalen" Variablen?
Globale Vairablen liegen im normalen SRam

Wenn eine Funktion aufgerufen wird, werden diese im Stack gehalten bis 
die Funktion beendet wird. Danach werden die Variablen vom Stack wieder 
gelöscht.

Der Heap ist wie du schon schreibst für dynamischen Speicher.

Kritisch wird es, wenn der Stack nicht mehr ausreicht oder auf den Heap 
prallt. Dann passieren unschöne Dinge.

von Ralf (Gast)


Lesenswert?

Hallo Tilo,

danke für deine Antwort.

> Der Stack liegt im S-Ram. Es gibt keine HW-Stack.
Das ist klar, so ist es auch beim 8051 (ich kenne keine Architektur, die 
einen dedizierten Stack hat). Mit Hardware-Stack meinte ich eben den für 
Rücksprungadressen etc. verwendeten Bereich.

> Was meinst du mit "lokalen" Variablen?
Variablen, welche innerhalb einer Funktion deklariert sind.

> Globale Vairablen liegen im normalen SRam
Auch klar (wo sonst? :) )

> Wenn eine Funktion aufgerufen wird, werden diese im Stack gehalten bis
> die Funktion beendet wird. Danach werden die Variablen vom Stack wieder
> gelöscht.
Und wie wird mit den Variablen auf dem Stack gearbeitet, wenn diese 
auf dem Stack sind? Wie ich geschrieben habe, ist für mich der Stack 
ein Bereich, auf dem Registerinhalte, etc. gesichert werden und somit 
eben nicht verändert werden sollen.
Wo ist mein Verständnisproblem?

> Kritisch wird es, wenn der Stack nicht mehr ausreicht oder auf den Heap
> prallt. Dann passieren unschöne Dinge.
Ebenfalls klar.

Ralf

von Peter D. (peda)


Lesenswert?

Der 8051 ist anders aufgebaut, als der ARM.

Der 8051 kann vieles direkt im RAM ausführen, daher kommt man besser, 
wenn lokale Variablen überlagert werden. Prinzipiell kann man aber auch 
Variablen im Stack anlegen und über @R0,1 drauf zugreifen. Man muß dann 
aber jedesmal R0,1 umladen, da er kein Displacement kennt.

Der ARM hingegen kann nichts im RAM machen, er muß alles erst in 
Register holen. Ein Überlagern hätte also keinen Vorteil, aber den 
Nachteil, daß Funktionen nicht reentrant sind.

von Marco S. (masterof)


Lesenswert?

Die PIC-Architektur (PIC16F88) von Microchip hat einen Hardwarestack.
Kann maximal 8 Adressen speichern.

von Axel S. (a-za-z0-9)


Lesenswert?

Tilo Lutz schrieb:
> Der Stack liegt im S-Ram. Es gibt keine HW-Stack.

Toll. Der Blinde erzählt dem Lahmen von Farben %-/

> Was meinst du mit "lokalen" Variablen?

Variablen der Storageklasse "auto". Die der Compiler bei gebräuchlichen 
Plattformen auf den Stack legt.


Ralf schrieb:
> Für einen 8051 ist der Stack nichts weiter als ein Speicher, auf dem
> Rücksprungadressen und Registerinhalte gesichert werden, beispielsweise
> beim Funktionsaufruf und in Interrupt-Service-Routinen.

Alle µC die über genug SRAM verfügen (oder externen RAM anbinden 
können), legen den Stack in diesen RAM. Nur sehr kleine Typen haben 
einen Hardwarestack, der i.d.R. auch sehr begrenzt ist. Ein Beispiel 
wäre der ATtiny12.

Wenn man genug Platz auf dem Stack hat, kann man außer Returnadressen 
für ISR und Funktionsaufrufe auch andere Daten auf den Stack legen. 
Insbesondere ist es praktisch, die lokalen Variablen einer Funktion auf 
dem Stack anzulegen. So ähnlich, wie man in Assembler den Stack 
verwendet um Register per PUSH auf den Stack zu sichern (und dann die 
Register für lokale Variablen zu verwenden). I.d.R. wird dazu im Prolog 
der Funktion ein sogenannter Stackframe erzeugt, also einfach der 
Stackpointer so weit vorgerückt, daß der notwendige Platz reserviert 
ist. Wenn die Funktion verlassen wird, wird das wieder rückgängig 
gemacht. Auf dem gleichen Weg kann man natürlich auch Argumente an die 
Funktion übergeben bzw. Ergebnisse zurückgeben.

Vorteil: funktioniert auch für Rekursion und die Speicherverwaltung ist 
pippileicht.

Es gibt auch Architekturen, die von Haus aus mehrere Stacks erlauben, 
z.B. in Form mehrerer Stackpointer. Da bietet es sich dann an, einen 
separaten Daten- und Return-Stack zu bauen.


XL

von Ralf (Gast)


Lesenswert?

@Peter Danneger:
> Der 8051 ist anders aufgebaut, als der ARM.
Ja, ich versuche gerade "umzusteigen", nur sollte ich wohl meine 
Erfahrung mit dem 8051 "vergessen", anstatt zu versuchen, sie "mit ins 
Boot zu nehmen" :)

> Der 8051 kann vieles direkt im RAM ausführen, daher kommt man besser,
> wenn lokale Variablen überlagert werden.
Wenn man das richtig macht, dann kann man selbst mit "nur" 256 Byte RAM, 
welche auch noch die Registerbänke, Stack, etc. beinhalten schon recht 
große Applikationen machen :)

> Prinzipiell kann man aber auch Variablen im Stack anlegen und über @R0,1
> drauf zugreifen. Man muß dann aber jedesmal R0,1 umladen, da er kein
> Displacement kennt.
Das ist auf nem 8051 aber ineffizient (wissen wir wohl beide :) ).

> Der ARM hingegen kann nichts im RAM machen, er muß alles erst in
> Register holen. Ein Überlagern hätte also keinen Vorteil, aber den
> Nachteil, daß Funktionen nicht reentrant sind.
Kann er nicht? Ich muss glaub ich weiter vorn anfangen und mal das 
InstructionSet genauer unter die Lupe nehmen.

@ Marco S.:
> Die PIC-Architektur (PIC16F88) von Microchip hat einen Hardwarestack.
> Kann maximal 8 Adressen speichern.
Wieder was dazu gelernt :)

@Axel:
> Wenn man genug Platz auf dem Stack hat, kann man außer Returnadressen
> für ISR und Funktionsaufrufe auch andere Daten auf den Stack legen.
> Insbesondere ist es praktisch, die lokalen Variablen einer Funktion auf
> dem Stack anzulegen. So ähnlich, wie man in Assembler den Stack
> verwendet um Register per PUSH auf den Stack zu sichern (und dann die
> Register für lokale Variablen zu verwenden). I.d.R. wird dazu im Prolog
> der Funktion ein sogenannter Stackframe erzeugt, also einfach der
> Stackpointer so weit vorgerückt, daß der notwendige Platz reserviert
> ist. Wenn die Funktion verlassen wird, wird das wieder rückgängig
> gemacht. Auf dem gleichen Weg kann man natürlich auch Argumente an die
> Funktion übergeben bzw. Ergebnisse zurückgeben.
Dann bin ich jetzt komplett verwirrt. Eins nach dem Anderen...
Du schreibst, dass die Register für lokale Variablen verwendet werden 
(hat Peter ja auch geschrieben, weil der Cortex-M wohl nicht direkt auf 
dem RAM arbeiten kann). Abgesehen davon, dass das m.V.n. ja dann auch 
für globale Variablen gelten muss, verstehe ich aber immer noch nicht, 
wie dann mit den Variablen auf dem Stack gearbeitet wird.

Ich glaub ich muss das mal an einem Beispiel versuchen zu erklären, wo 
ich die Blockade habe:
Sagen wir eine Funktion hat drei lokale Variablen. Ich rufe die Funktion 
auf. Was passiert dann? Wenn es heisst "die lokalen Variablen werden auf 
dem Stack erzeugt", dann verstehe ich darunter simpel ausgedrückt drei 
PUSH-Instruktionen. Und jetzt? Die Dinger sind auf dem Stack, der für 
mich immer noch eine Sicherung bestehender Werte von RAM bzw. 
Registern ist.
Wie kann dann beispielsweise mit der ersten der drei Variablen 
gearbeitet werden, wenn sie an dritter Position im Stack liegt (unter 
der Annahme dass die erste Variable auch die erste ist, die auf dem 
Stack gelandet ist)? Von einem Stack kann man doch nur die zuletzt auf 
dem Stack gesicherte Variable holen, aber sie dort doch nicht 
verändern, geschweige denn die Variablen, die tiefer im Stack liegen.
Versteht ihr jetzt meine Denkblockade?

> Es gibt auch Architekturen, die von Haus aus mehrere Stacks erlauben,
> z.B. in Form mehrerer Stackpointer. Da bietet es sich dann an, einen
> separaten Daten- und Return-Stack zu bauen.
Okay, der Cortex-M hat ja zwei Stackpointer, aber die sind nicht 
getrennt als Daten- und Return-Stack zu verwenden.

Ralf

von Helmut L. (helmi1)


Lesenswert?

Ralf schrieb:
> Von einem Stack kann man doch nur die zuletzt auf
> dem Stack gesicherte Variable holen, aber sie dort doch nicht
> verändern, geschweige denn die Variablen, die tiefer im Stack liegen.

Der Stack ist doch auch nur normales RAM und hat damit auch für jede 
Variabel eine Addresse. Vergiss das mal mit dem Push und Pop Befehlen.
Man setzt einen Indexpointer auf die Variablen  im Stack und addresiert 
sie mit Displacment und schon kommt man dran. Die Variablen legt man auf 
dem Stack an in dem man den Stackpointer manipuliert also vor dem 
Eintritt in die Funktion den Stackpointer um die Grösse der Variabeln 
subtraiert und beim verlassen dementsprechend den SP addiert.

von Achim S. (Gast)


Lesenswert?

Ralf schrieb:
> Von einem Stack kann man doch nur die zuletzt auf
> dem Stack gesicherte Variable holen, aber sie dort doch nicht
> verändern, geschweige denn die Variablen, die tiefer im Stack liegen.
> Versteht ihr jetzt meine Denkblockade?

Schau dir mal die Speicherzugriffe/Adressierung "Load and Store with 
immediate offset" an. Dabei nutzt du ein Register (in diesem Fall den 
Stackpointer) zum Adressieren des SRAMs. Und der immediate Offset legt 
fest, ob du auf die dritte oder zweite Variable "vor der 
Rücksprungadresse" zugreifst.

Könnte man nur per Push und Pop auf den Stack zugreifen, käme man 
tatsächlich nicht vernünftig an die tiefer im Stack liegenden Variablen 
ran.

von Ralf (Gast)


Lesenswert?

@Helmut:
> Der Stack ist doch auch nur normales RAM und hat damit auch für jede
> Variable eine Addresse.
Ja, das ist klar.

> Vergiss das mal mit dem Push und Pop Befehlen.
Werd ich wohl müssen :)

> Man setzt einen Indexpointer auf die Variablen  im Stack und addresiert
> sie mit Displacment und schon kommt man dran. Die Variablen legt man auf
> dem Stack an in dem man den Stackpointer manipuliert also vor dem
> Eintritt in die Funktion den Stackpointer um die Grösse der Variabeln
> subtraiert und beim verlassen dementsprechend den SP addiert.
Mmmmh... ich glaube das erste Aufglimmen von Verständnis kommt. Mal 
schauen ob wir ein Licht draus machen können:
- man (bzw.der Compiler) berechnet den Platzbedarf der Variablen und 
verschiebt den Stackpointer entsprechend (damit zwischendurch 
auftretende Interrupts oder aufgerufene Funktion die Variablen nicht 
zerstören und der Stack sauber arbeitet
- der "frei" gewordene Stack-Speicher bildet die Variablen ab. 
Zugegriffen wird dann über einen zum Stackpointer relativen Pointer? 
Oder wie wird der von dir beschriebene "Indexpointer" gebildet? Er muss 
ja dann zum Stackpointer relativ sein, sonst können die Funktionen nicht 
reentrant sein, korrekt?
- Die entsprechenden Werte werden dann in die Register geholt, dort nach 
Bedarf manipuliert (weil keine direkten Operationen auf dem RAM möglich 
sind) und wieder zurückgeschrieben, richtig?

Wie nahe kommt das der Wahrheit? :)

Ralf

von Ralf (Gast)


Lesenswert?

Edit:
@Helmut:
Achim hat mir m.E. meinen Post gerade bestätigt.

@Achim:
> Schau dir mal die Speicherzugriffe/Adressierung "Load and Store with
> immediate offset" an. Dabei nutzt du ein Register (in diesem Fall den
> Stackpointer) zum Adressieren des SRAMs. Und der immediate Offset legt
> fest, ob du auf die dritte oder zweite Variable "vor der
> Rücksprungadresse" zugreifst.
Vielen Dank, das deckt sich mit meiner Antwort an Helmut.
Und wie ich gesagt habe, ich muss wirklich mal das InstructionSet 
herholen.

> Könnte man nur per Push und Pop auf den Stack zugreifen, käme man
> tatsächlich nicht vernünftig an die tiefer im Stack liegenden Variablen
> ran.
Ja, eben, das war dann auch meine o.g. Blockade! Aber jetzt wird die 
Sache viiiieeeel klarer :)


Vielen Dank an alle.

Ralf

von Karl H. (kbuchegg)


Lesenswert?

> - der "frei" gewordene Stack-Speicher bildet die Variablen ab.
> Zugegriffen wird dann über einen zum Stackpointer relativen Pointer?
> Oder wie wird der von dir beschriebene "Indexpointer" gebildet? Er muss
> ja dann zum Stackpointer relativ sein, sonst können die Funktionen nicht
> reentrant sein, korrekt?

Jetzt hast du es.
Eigentlich ganz einfach.

Die lokalen Variablen haben keine ständige fixe Adresse, sondern für sie 
wird Speicher 'reserviert' wenn die Funktion aufgerufen wird und der 
Speicher wird wieder freigegeben, wenn die Funktion den Return macht. 
Und was bietet sich da besseres an, als den Stack mitzubenutzen, auf dem 
man sich unaufwändig und geordnet ein paar Bytes nach Bedarf reservieren 
kann, indem man den Stackpointer einfach ein paar Bytes weiter runter 
bzw. raufzählt.

von Klaus (Gast)


Lesenswert?

Axel Schwenke schrieb:
> Vorteil: funktioniert auch für Rekursion

Ist das für C nicht Pflicht?

MfG Klaus

von (prx) A. K. (prx)


Lesenswert?

Diese Technik, lokale Daten im Stack zu speichern, findet man bei wohl 
allen aktuellen Prozessor-Architekturen jenseits der 8-Bit Klasse und 
auch bei einigen 8-Bittern. Also auch bei PC-Prozessoren.

Grundvoraussetzung ist aber, dass der Prozessor effizient relativ zu 
einem Register oder direkt dem Stackpointer adressieren kann. 8051 kann 
das nicht, weshalb C Compiler dafür anders arbeiten müssen, wenn der 
Code effizient sein soll. Du bist in dieser Hinsicht also von der 
Ausnahme (8051) zur Regel umgestiegen.

von Reinhard Kern (Gast)


Lesenswert?

Ralf schrieb:
> Zugegriffen wird dann über einen zum Stackpointer relativen Pointer?
> Oder wie wird der von dir beschriebene "Indexpointer" gebildet?

Das hängt vom Befehlssatz ab. Wenn man Stackpointer-relativ adressieren 
kann, braucht man nichts weiter, in den meisten Fällen verwendet man 
eines der Pointer-Register mit Offset, das beim Eintritt in das 
Unterprogramm auf den Wert des SP gesetzt wird. Da man so selbst in der 
Hand hat welches Register, muss man mit anderen Schreiberlingen an der 
gleichen Software darüber garnicht einig sein, trotzdem hat sich oft für 
einen bestimmten Prozessor ein bestimmtes Register eingebürgert.

Was man natürlich wissen muss, ist das Muster, nach dem die lokalen 
Variablen gespeichert werden, bei der C-Konvention in der Reihenfolge in 
der sie aufgeführt werden, bei Pascal umgekehrt. Danach richtet sich 
dann der Offset bezüglich der Rücksprungaddresse.

Die Frage, warum überhaupt: lokale Variablen gehören eben zum 
Unterprogramm, sie leben genauso lang; sie werden ab dem Sprung dorthin 
gebraucht und nach dem Rücksprung nicht mehr, man kann sie also einfach 
auf dem Stack verwalten ohne dass unpraktische Lücken im Speicher 
entstehen.

Gruss Reinhard

von (prx) A. K. (prx)


Lesenswert?

Reinhard Kern schrieb:
> Die Frage, warum überhaupt: lokale Variablen gehören eben zum
> Unterprogramm, sie leben genauso lang; sie werden ab dem Sprung dorthin
> gebraucht und nach dem Rücksprung nicht mehr, man kann sie also einfach
> auf dem Stack verwalten ohne dass unpraktische Lücken im Speicher
> entstehen.

Das ist ein Grund. Es ist aber auch ungemein platzsparend. Einem x86 
reicht meist 1 Offset-Byte aus, um lokale Daten relativ zum Stackpointer 
zu adressieren. Für statische Daten braucht er jedoch 4 Bytes. Ein ARM 
ist bei statischer Adressierung noch schlimmer dran, weil der eine feste 
32-Bit Adresse erst irgendwie ins Register zaubern muss, bevor er die 
Daten selbst laden/speichern kann (Cortext-M3/4: 6-8 Bytes pro Adresse).

Dazu kommt heutzutage, dass diese Methode dem Verhalten von Caches sehr 
entgegen kommt.

von Peter D. (peda)


Lesenswert?

Den ARM möchte man nicht wirklich in Assembler programmieren, da RISC.

Wenn Du z.B. beim 8051 schreibst:
- DJNZ Variable, Zieladresse

Sieht das gleiche beim ARM etwa so aus:
- Lade R1 mit Variablen-Adresse Low-Word
- Addiere zu R1 die Variablen-Adresse High-Word
- Lade R2 über R1-Pointer mit der Variable
- DEC R2
- Store R2 über R1-Pointer nach Variable
- JNZ nach Ziel


Lasse also besser den C-Compiler diesen ganzen umständlichen Wust an 
Code erzeugen. Und dann brauchst Du Dich auch nicht um den Stack zu 
kümmern.

von (prx) A. K. (prx)


Lesenswert?

Peter, jetzt überziehst du. Dem DJNZ des 8051 entspricht 
realistischerweise etwas wie
   subs r0,r0,#1
   bne ...
weil jeder nicht völlig bescheuerte Compiler solche Variablen erst 
garnicht in den Speicher legt, sondern ins Register. Es sei denn der 
Schleifenzähler ist eine globale/statische Variable - aber wer macht 
sowas schon.

Insgesamt stimmt aber, dass Assembler-Programmierung für RISCs 
umständlicher ist, weil deutlich mehr Operationen in mehrere Befehle 
aufgeteilt werden müssen als bei CISCs.

von Peter D. (peda)


Lesenswert?

A. K. schrieb:
> Dem DJNZ des 8051 entspricht
> realistischerweise etwas wie
>    subs r0,r0,#1
>    bne ...

Nö.
Z.B. zähle ich damit im 10ms Interrupt, bis 1s rum ist.
DJNZ ist in Interrupts besonders praktisch, da es keine Statusbits 
verändert.

Ich wollte schon den allgemeinen Fall aufzeigen, daß die Variable im RAM 
liegt bzw. im Stackframe.
Daß der Compiler vesucht, häufige lokale Variablen in Registern zu 
halten, ist der Sonderfall.
Oft reichen die Register auch nicht aus, um alle lokalen Variablen in 
Registern zu halten.

von Axel S. (a-za-z0-9)


Lesenswert?

OK, nachdem der allgemeine Mechanismus jetzt hoffentlich klar ist, noch 
ein bisschen Folklore. Der Begriff Stack Pointer ist ja wohl klar, 
ebenfalls haben wir geklärt daß lokale Variablen (und Argumente) in 
einem auf dem Stack reservierten Speicherbereich namens Stack Frame 
vorgehalten werden.

Der direkt verwandte Begriff ist Frame Pointer. Das ist im Prinzip ein 
Zeiger, der den gerade aktiven Stack Frame referenziert (je nach 
Wachstumsrichtung des Stacks und vorhandenen Adressierungsmodi den 
Anfang oder das Ende). Objekte innerhalb des Stack Frames (aka lokale 
Variablen) werden dann relativ zum Frame Pointer adressiert. 
Hochsprachentaugliche CPUs haben dafür ein extra Register (oder gleich 
mehrere). Z.B. wird auf x86 dafür gerne das Buffer Pointer (BP) Register 
verwendet.

Wenn man jetzt im Prolog einer Funktion den Frame Pointer per push auf 
den Stack schiebt, dann den Stack Pointer in den Frame Pointer kopiert 
und relativ zum Frame Pointer den Stack Frame aufbaut, bekommt man 
Reentranz, Garbage Collection und Pipapo quasi kostenlos.

Der gcc-Schalter -f[no-]omit-frame-pointer sagt dem Compiler, ob er den 
Frame Pointer permanent in einem Register halten soll (praktisch für das 
Debuggen) oder ob er bei hoher Registerlast den Frame Pointer auch mal 
verwerfen (und bei Bedarf rekonstruieren) soll.

PS: "frame pointer" bietet sich auch als Google-Stichwort an


XL

von (prx) A. K. (prx)


Lesenswert?

Peter Dannegger schrieb:
> Daß der Compiler vesucht, häufige lokale Variablen in Registern zu
> halten, ist der Sonderfall.

Wie bitte??? Wenn er das bei RISC (oder x86) nicht tut, dann
 Tonne auf,
 rein damit,
 Tonne zu.
Registeroptimierung ist eine Kernaufgabe von Compilern. RISC Prozessoren 
ergeben nur in Verbindung damit überhaupt einen Sinn.

Aber die Art eines Programms und die verwendete Programmiertechnik 
spielen dabei eine erhebliche Rolle. Kleine Controllerprogramme können 
stark von globalen Variablen geprägt sein, und da läuft es klar in deine 
Richtung. Bei grossen Programmen sollte man freilich nicht mit Myriaden 
globaler Skalarvariablen hantieren. Zudem kann ein Compiler ggf. auch 
globale Variablen übergangsweise in Register legen, wenn er sich sicher 
ist, dass niemand sonst mitmischt.

Grosse Programme sind von register-relativer Adressierung geprägt, weil 
eher verwaltete Datenstrukturen als globale Skalare dominieren.

von Peter D. (peda)


Lesenswert?

A. K. schrieb:
> Wie bitte??? Wenn er das bei RISC (oder x86) nicht tut, dann
>  Tonne auf,
>  rein damit,
>  Tonne zu.

Du erzählst Quatsch.
Es ging hier doch um Variablen im Stack, d.h. wenn dem Compiler die 
Register eben nicht mehr reichen. Ein Compiler kann doch keine 
weiteren Register hervorzaubern.
Die wenigsten Funktionen bestehen nur aus 3 Zeilen Code.

Ein guter Compiler sollte abschätzen können, welche Variablen am 
häufigsten zugegriffen werden.
Oftmals hat man auch mehrere Schleifen ineinander. Da wird der Compiler 
möglichst die innere Schleife in Registern ausführen.
Der äußere Schleifenzähler kann durchaus im RAM landen, da ja viel 
seltener zugegriffen.


A. K. schrieb:
> Registeroptimierung ist eine Kernaufgabe von Compilern.

Das wußte ja schon der olle Keil C51. Aber irgendwann sind die Register 
belegt und dann nimmt er SRAM.
Er hat natürlich weniger Reserve, da nur 8 Bit. D.h. die 8 Register 
können nur 2 long Werte fassen.

von (prx) A. K. (prx)


Lesenswert?

Peter Dannegger schrieb:
> Es ging hier doch um Variablen im Stack, d.h. wenn dem Compiler die
> Register eben nicht mehr reichen.

Das hatte ich in diesem Thread bisher nicht so empfunden. Mein Eindruck 
war vielmehr, dass lokale Variablen hier der Einfachheit halber mit 
Stack-Variablen gleichgesetzt wurden.

> Ein Compiler kann doch keine weiteren Register hervorzaubern.

In gewisser Weise schon. Ein guter Compiler wird nicht notwendigerweise 
eine Variable für die Dauer einer Funktion fest mit einem Register 
verknüpfen, sondern wird ggf. ein Register an verschiedenen Stellen der 
Funktion als "Cache" für verschiedene Variablen verwenden. Der Platz auf 
dem Stack wird dann zwar benötigt, spielt aber keine wesentliche Rolle 
in der Laufzeit.

Es ist freilich auch klar, dass ein RISC Prozessor mehr Register 
benötigt, als beispielsweise ein Akkumulator-Architektur. Allerdings hat 
er deshalb auch mehr davon.

von Peter D. (peda)


Lesenswert?

Ich hab selber nicht ARM Assembler gemacht, sondern nur das Listing 
angesehen.
Wenn man mal ein einfaches Blinkprogramm mit Timerinterrupt nimmt, 
strotzt das nur so von Load/Store Overhead.

Gut, IO-Zugriffe darf er ja nicht wegoptimieren.
Aber ich hätte schon erwartet, daß der Compiler bei nem Schwung IO- oder 
SRAM-Zugriffe analysiert, welches wäre die beste Base-Adresse, um mit 
Displacement die meisten IO-Register zu erreichen.

Die neueren Compilerversionen geben ja mit Structs die Base-Adresse vor, 
zu Lasten einer umständlicheren, langatmigen Schreibweise. Aber das auch 
nur für je ein IO-Modul.
Ein Displacement könnte jedoch mit einem Base-Pointer durchaus mehrere 
benachbarte IO-Module zugreifen.

Der Compiler sollte mir auch die ganzen Derivat spezifischen 
Sonderbefehle abnehmen (z.B. Aliasadressen für IO-Bits, Clear-Register), 
so daß ich alles in plain C hinschreiben darf und er optimiert das 
Derivat spezifisch.

von Axel S. (a-za-z0-9)


Lesenswert?

A. K. schrieb:
> Peter Dannegger schrieb:
>> Es ging hier doch um Variablen im Stack, d.h. wenn dem Compiler die
>> Register eben nicht mehr reichen.
>
> Das hatte ich in diesem Thread bisher nicht so empfunden. Mein Eindruck
> war vielmehr, dass lokale Variablen hier der Einfachheit halber mit
> Stack-Variablen gleichgesetzt wurden.

Kein Wunder, ging es doch in diesem Thread vor dem Kaperversuch durch 
die Compilerversteherfraktion :P um die Verwendung des Stacks abseits 
reiner PUSH/POP, CALL/RET Sequenzen.

Daß es effizienter ist, lokale Variablen und Funktionsargumente gleich 
und ausschließlich in Registern zu halten, bleibt davon natürlich 
unberührt.

----
Aber wenn wir ohnehin beim Abschweifen sind, erzähle ich noch von meiner 
ersten Begegnung mit Stackmanipulationen. Und zwar war das, als ich 
erstmals eine PRIMM (PRint IMMediate) Funktion in Assembler gesehen 
habe. Damals muß ich so 14 gewesen sein.

Die Funktion kommt zur Anwendung, wenn man aus einem Assemblerprogramm 
heraus einen Text z.B. auf den Bildschirm ausgeben will. Das sieht dann 
so aus:
1
        CALL primm
2
        DB "Hallo Welt!", 0
3
        ;hier gehts weiter

Das Null-terminierte Stringliteral steht hier mitten im Code.
Eine Implementierung für Z80 könnte z.B. so aussehen:
1
primm:  EX (SP), HL
2
pr1:    LD A, (HL)
3
        INC HL
4
        OR A
5
        JRZ pr2
6
        CALL char_out
7
        JR pr1
8
pr2:    EX (SP), HL
9
        RET

Sollte selbsterklärend sein.

Die Alternative wäre, alle Stringliterale zentral an einer Stelle 
abzulegen und dann bei der Ausgabe sowas zu schreiben
1
        LD HL, string_0815
2
        CALL print_asciiz

Der Vorteil von PRIMM ist, daß man beim Lesen des Codes sofort sieht was 
passiert und sich nicht erst über die "string_0815" Marke zum String 
durchhangeln muß.


XL

von (prx) A. K. (prx)


Lesenswert?

Peter Dannegger schrieb:
> Wenn man mal ein einfaches Blinkprogramm mit Timerinterrupt nimmt,
> strotzt das nur so von Load/Store Overhead.

Was schätzt du: Wieviel Prozent eines normalen 100KB 
Controller-Programmes machen direkte I/O-Zugriffe aus?

I/O-Befehle hat ARM nicht. Der Aufwand um ein I/O-Bit anzusprechen ist 
viel höher als bei 8051. 8051 wurde direkt dafür entwickelt, ARM nicht.

Andererseits: Grössere Programme haben grössere Datenstrukturen. Arrays, 
Structs, verpointerter Kram, Hashtables, ... Nicht viel anders als bei 
PCs. Und da sind ARMs dann nicht so übel. Daten werden nicht 
hauptsächlich mit absoluter Adresse angesprochen, sondern meistens 
relativ zu einem Pointer. Die meisten solcher Daten sind dann auch nicht 
volatile, wodurch Optimierung möglich wird.

Der Eindruck, dass ARM viel mehr Code braucht, der trügt. In kleinem 
Umfeld mit hohem Anteil von I/O-Zugriffen stimmt das noch, aber darüber 
kehrt sich das um.

> Ein Displacement könnte jedoch mit einem Base-Pointer durchaus mehrere
> benachbarte IO-Module zugreifen.

Nein. ARM-Displacements adressieren +/-4095 Bytes, Thumb-v1 weniger. Da 
ein einzelnes I/O-Modul bei ARMs stets einen 4KB Adressraum einnimmt, 
ist es nie der Fall, dass eine Basisadresse sich für mehr als ein Modul 
eignet.

Allerdings könnte der Compiler in der Funktion einmal eine Adresse 
innerhalb des Moduls laden und später alles darin über dieses Register 
adressieren. Daran hapert es etwas.

von Peter D. (peda)


Lesenswert?

Axel Schwenke schrieb:
> Das Null-terminierte Stringliteral steht hier mitten im Code.

Ja, so habe ich es zu meinen 8051 Assemblerzeiten auch gemacht:
1
;--------------------------------------------------------------------------
2
;       UP: Ausgabe des Textes im Flash nach Aufruf (inline)
3
;       ====================================================
4
;
5
lcd_text_inline:
6
        pop     dph                     ; hole Adresse des Textes
7
        pop     dpl
8
        jmp     _lcdti2
9
_lcdti1:
10
        call    lcd_data                ; Ausgabe eines Bytes ans LCD  
11
_lcdti2:
12
        clr     a                       ; kein Offest
13
        movc    a, @a+dptr              ; lese Byte aus Flash
14
        inc     dptr                    ; zeige auf naechstes Byte
15
        jnz     _lcdti1                 ; ungleich 0 -> Anzeigen
16
        jmp     @a+dptr                 ; springe zurueck hinter das 0-Byte
17
;--------------------------------------------------------------------------

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.