Die meisten Architekturen, die ich gesehen habe, basieren auf einem Aufrufstapel, um den Kontext vor Funktionsaufrufen zu speichern / wiederherzustellen. Es ist ein so verbreitetes Paradigma, dass Push- und Pop-Operationen in den meisten Prozessoren integriert sind. Gibt es Systeme, die ohne Stack funktionieren? Wenn ja, wie funktionieren sie und wofür werden sie verwendet?
computer-architecture
ConditionRacer
quelle
quelle
Antworten:
Eine (etwas) beliebte Alternative zu einem Call-Stack sind Fortsetzungen .
Die Parrot-VM basiert beispielsweise auf Fortsetzungen. Es ist vollständig stapelfrei: Daten werden in Registern gespeichert (wie Dalvik oder LuaVM, Parrot ist registergestützt), und der Kontrollfluss wird mit Fortsetzungen dargestellt (im Gegensatz zu Dalvik oder LuaVM, die über einen Aufrufstapel verfügen).
Eine weitere beliebte Datenstruktur, die in der Regel von Smalltalk- und Lisp-VMs verwendet wird, ist der Spaghetti-Stack, der einem Netzwerk von Stacks ähnelt.
Wie @rwong hervorhob, ist der Continuation-Passing-Stil eine Alternative zu einem Call-Stack. Programme, die im Continuation-Passing-Stil geschrieben (oder in diesen transformiert) wurden, kehren nie zurück, sodass kein Stapel erforderlich ist.
Beantworten Sie Ihre Frage aus einer anderen Perspektive: Es ist möglich, einen Aufrufstapel ohne einen separaten Stapel zu haben, indem Sie die Stapelrahmen auf dem Heap zuweisen. Einige Lisp- und Scheme-Implementierungen tun dies.
quelle
Früher hatten Prozessoren keine Stapelanweisungen, und Programmiersprachen unterstützten keine Rekursion. Im Laufe der Zeit unterstützen immer mehr Sprachen die Rekursion und die Hardware-Suite mit Funktionen zur Stapelrahmenzuweisung. Diese Unterstützung war im Laufe der Jahre bei verschiedenen Prozessoren sehr unterschiedlich. Einige Prozessoren verwendeten Stapelrahmen- und / oder Stapelzeigerregister; Einige übernommene Befehle, die die Zuweisung von Stapelrahmen in einem einzigen Befehl ausführen würden.
Ein entscheidender Vorteil des Stacks ist die Cache-Lokalität, da Prozessoren erst mit Caches auf einer Ebene und dann mit Caches auf mehreren Ebenen aufwarten. Die Spitze des Stapels befindet sich fast immer im Cache. Wann immer Sie etwas tun können, das eine hohe Cache-Trefferrate aufweist, sind Sie mit modernen Prozessoren auf dem richtigen Weg. Der auf den Stack angewendete Cache bedeutet, dass sich lokale Variablen, Parameter usw. fast immer im Cache befinden und das höchste Leistungsniveau aufweisen.
Kurz gesagt, die Verwendung des Stacks hat sich sowohl in der Hardware als auch in der Software entwickelt. Es gibt andere Modelle (z. B. wurde Datenflussberechnung über einen längeren Zeitraum versucht), die Lokalität des Stapels macht es jedoch sehr gut. Außerdem ist prozeduraler Code genau das, was Prozessoren für die Leistung benötigen: Eine Anweisung gibt an, was nach der anderen zu tun ist. Wenn die Anweisungen nicht in linearer Reihenfolge vorliegen, verlangsamt sich der Prozessor, zumindest bis jetzt, enorm, da wir nicht herausgefunden haben, wie der Direktzugriff so schnell wie der sequentielle Zugriff erfolgen kann. (Übrigens gibt es auf jeder Speicherebene ähnliche Probleme, vom Cache über den Hauptspeicher bis hin zur Disc ...)
Zwischen der nachgewiesenen Leistung von sequentiellen Zugriffsanweisungen und dem vorteilhaften Caching-Verhalten des Aufrufstapels haben wir zumindest derzeit ein gewinnendes Leistungsmodell.
(Wir könnten auch die Veränderbarkeit von Datenstrukturen in die Werke werfen ...)
Dies bedeutet nicht, dass andere Programmiermodelle nicht funktionieren, insbesondere wenn sie in die sequentiellen Anweisungen und das Call-Stack-Modell der heutigen Hardware übersetzt werden können. Es gibt jedoch einen deutlichen Vorteil für Modelle, die unterstützen, wo sich die Hardware befindet. Die Dinge bleiben jedoch nicht immer gleich, sodass wir in Zukunft Änderungen sehen können, da verschiedene Speicher- und Transistortechnologien mehr Parallelität ermöglichen. Es ist immer eine Scherze zwischen Programmiersprachen und Hardware-Fähigkeiten, also werden wir sehen!
quelle
TL; DR
Der Rest dieser Antwort ist eine zufällige Ansammlung von Gedanken und Anekdoten und daher etwas unorganisiert.
Der Stapel, den Sie (als Funktionsaufrufmechanismus) beschrieben haben, ist spezifisch für die imperative Programmierung.
Unterhalb der erforderlichen Programmierung finden Sie den Maschinencode. Maschinencode kann den Aufrufstapel durch Ausführen einer kleinen Folge von Anweisungen emulieren.
Unterhalb des Maschinencodes finden Sie die Hardware, die für die Ausführung der Software verantwortlich ist. Während der moderne Mikroprozessor zu komplex ist, um hier beschrieben zu werden, kann man sich vorstellen, dass ein sehr einfacher Entwurf existiert, der langsam ist, aber immer noch denselben Maschinencode ausführen kann. Solch ein einfaches Design nutzt die Grundelemente der digitalen Logik:
Die folgenden Diskussionen enthielten zahlreiche Beispiele für alternative Arten der Strukturierung von Imperativprogrammen.
Die Struktur eines solchen Programms sieht folgendermaßen aus:
Dieser Stil ist für Mikrocontroller geeignet, dh für diejenigen, die die Software als Ergänzung zu den Funktionen der Hardware betrachten.
quelle
Nein, nicht unbedingt.
Lesen Sie Appels Garbage Collection- Altpapier kann schneller sein als die Stapelzuweisung . Es verwendet Continuation-Passing-Stil und zeigt eine stapellose Implementierung.
Beachten Sie auch, dass alte Computerarchitekturen (z. B. IBM / 360 ) kein Hardware-Stack-Register hatten. Aber das O und Compiler reservierten ein Register für den Stapelzeiger durch Konvention (bezogen auf Aufrufkonventionen ) , so dass sie einen Software haben könnten Call - Stack .
Im Prinzip kann ein C-Compiler und -Optimierer des gesamten Programms den Fall erkennen (was bei eingebetteten Systemen häufig vorkommt), dass der Aufrufgraph statisch bekannt ist und keine Rekursion (oder Funktionszeiger) aufweist. In einem solchen System konnte jede Funktion ihre Absenderadresse an einem festen statischen Ort aufbewahren (und so funktionierte Fortran77 in den 1970er Jahren).
Heutzutage haben Prozessoren auch Aufrufstapel (und Anweisungen zum Aufrufen und Zurückgeben von Maschinen), die sich der CPU-Caches bewusst sind .
quelle
SUBROUTINE
und benötigteFUNCTION
. Sie sind jedoch korrekt für frühere Versionen (FORTRAN-IV und möglicherweise WATFIV).TR
undTRT
.Sie haben bis jetzt einige gute Antworten; Lassen Sie mich Ihnen ein unpraktisches, aber sehr lehrreiches Beispiel dafür geben, wie Sie eine Sprache ohne den Begriff Stapel oder "Kontrollfluss" überhaupt entwerfen können. Hier ist ein Programm, das Fakultäten ermittelt:
Wir setzen dieses Programm in eine Zeichenfolge und bewerten das Programm durch Textsubstitution. Wenn wir also auswerten
f(3)
, führen wir eine Suche durch und ersetzen sie durch 3 für i, wie folgt:Toll. Jetzt führen wir eine weitere Textsubstitution durch: Wir stellen fest, dass die Bedingung des "if" falsch ist, und ersetzen eine weitere Zeichenfolge, wodurch das Programm erzeugt wird:
Jetzt ersetzen wir alle Unterausdrücke mit Konstanten durch einen anderen String:
Und Sie sehen, wie das geht; Ich werde nicht weiter darauf eingehen. Wir könnten so lange eine Reihe von Saitensubstitutionen durchführen, bis wir
let x = 6
fertig sind.Wir verwenden den Stack traditionell für lokale Variablen und Fortsetzungsinformationen. Denken Sie daran, ein Stapel sagt Ihnen nicht, woher Sie gekommen sind, sondern wohin Sie mit diesem Rückgabewert als Nächstes gehen.
Im Zeichenfolgensubstitutionsmodell der Programmierung befinden sich keine "lokalen Variablen" auf dem Stapel. Die formalen Parameter werden durch ihre Werte ersetzt, wenn die Funktion auf ihr Argument angewendet wird, anstatt in eine Nachschlagetabelle auf dem Stapel abgelegt zu werden. Und es gibt kein "irgendwohin als nächstes", weil die Programmbewertung einfach einfache Regeln für die String-Ersetzung anwendet, um ein anderes, aber gleichwertiges Programm zu erstellen.
Natürlich ist das Ersetzen von Strings nicht der richtige Weg. Aber Programmiersprachen, die "Gleichheitsdenken" unterstützen (wie Haskell), verwenden logischerweise diese Technik.
quelle
Seit der Veröffentlichung von " Über die Kriterien für die Zerlegung von Systemen in Module" durch Parnas im Jahr 1972 wurde vernünftigerweise akzeptiert, dass in Software versteckte Informationen eine gute Sache sind. Dies folgt auf eine lange Debatte in den 60er Jahren über strukturelle Zersetzung und modulare Programmierung.
Modularität
Ein notwendiges Ergebnis von Black-Box-Beziehungen zwischen Modulen, die von verschiedenen Gruppen in einem Multithread-System implementiert werden, erfordert einen Mechanismus, der einen Wiedereintritt ermöglicht, und ein Mittel zum Verfolgen des dynamischen Aufrufgraphen des Systems. Der kontrollierte Ausführungsfluss muss in mehrere Module hinein und aus diesen heraus gehen.
Dynamisches Scoping
Sobald das lexikalische Scoping nicht ausreicht, um das dynamische Verhalten zu verfolgen, ist eine gewisse Laufzeitbuchhaltung erforderlich, um den Unterschied zu verfolgen.
Wenn ein Thread (per Definition) nur einen einzigen aktuellen Befehlszeiger hat, ist ein LIFO-Stack geeignet, um jeden Aufruf zu verfolgen.
Ausnahmen
Während das Fortsetzungsmodell keine explizite Datenstruktur für den Stapel verwaltet, gibt es dennoch den verschachtelten Aufruf von Modulen, die irgendwo verwaltet werden müssen!
Selbst deklarative Sprachen behalten entweder den Evaluierungsverlauf bei oder reduzieren den Ausführungsplan aus Leistungsgründen und halten den Fortschritt auf andere Weise aufrecht.
Die von rwong identifizierte Endlosschleifenstruktur ist in Anwendungen mit hoher Zuverlässigkeit und statischer Planung üblich, die viele übliche Programmierstrukturen nicht zulassen, jedoch erfordern, dass die gesamte Anwendung als White Box ohne signifikante versteckte Informationen betrachtet wird.
Mehrere gleichzeitige Endlosschleifen erfordern keine Struktur zum Speichern von Rücksprungadressen, da sie keine Funktionen aufrufen, was die Frage zur Diskussion stellt. Wenn sie mit gemeinsam genutzten Variablen kommunizieren, können diese leicht zu älteren analogen Absenderadressen im Fortran-Stil ausarten.
quelle
Alle alten Mainframes (IBM System / 360) hatten überhaupt keine Vorstellung von einem Stack. Auf dem 260 wurden beispielsweise Parameter an einer festen Stelle im Speicher erstellt, und wenn ein Unterprogramm aufgerufen wurde, wurde es mit einem
R1
Verweis auf den Parameterblock undR14
der Rücksprungadresse aufgerufen . Wenn die aufgerufene Routine eine andere Unterroutine aufrufen möchte, muss sie anR14
einem bekannten Ort gespeichert werden, bevor dieser Aufruf erfolgt.Dies ist viel zuverlässiger als ein Stapel, da alles an festen Speicherorten gespeichert werden kann, die zur Kompilierungszeit eingerichtet wurden, und es kann zu 100% garantiert werden, dass die Prozesse niemals den Stapel verlieren. Es gibt keine der "Allocate 1MB und Daumen drücken", die wir heutzutage tun müssen.
Rekursive Unterprogrammaufrufe wurden in PL / I durch Angabe des Schlüsselworts zugelassen
RECURSIVE
. Sie bedeuteten, dass der von der Unterroutine verwendete Speicher eher dynamisch als statisch zugewiesen wurde. Aber rekursive Aufrufe waren damals genauso selten wie heute.Der stapellose Betrieb erleichtert auch das massive Multithreading erheblich, weshalb häufig versucht wird, moderne Sprachen stalklos zu machen. Es gibt zum Beispiel keinen Grund, warum ein C ++ - Compiler nicht so modifiziert werden könnte, dass er dynamisch zugewiesenen Speicher anstelle von Stapeln verwendet.
quelle