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
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.
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
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.
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
@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
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.
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.
@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
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
> - 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.
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.
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
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.
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.
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.
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.
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
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.
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.
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.
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.
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
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.