UPDATE: Diese Frage hat mir so gut gefallen, dass ich sie am 18. November 2011 zum Thema meines Blogs gemacht habe . Danke für die tolle Frage!
Ich habe mich immer gefragt: Was ist der Zweck des Stapels?
Ich gehe davon aus, dass Sie den Evaluierungsstapel der MSIL-Sprache meinen und nicht den tatsächlichen Pro-Thread-Stapel zur Laufzeit.
Warum gibt es eine Übertragung vom Speicher zum Stapel oder "Laden"? Warum gibt es andererseits eine Übertragung vom Stapel in den Speicher oder ein "Speichern"? Warum nicht einfach alle in Erinnerung behalten?
MSIL ist eine Sprache der "virtuellen Maschine". Compiler wie der C # -Compiler generieren CIL , und zur Laufzeit wandelt ein anderer Compiler namens JIT-Compiler (Just In Time) die IL in tatsächlichen Maschinencode um, der ausgeführt werden kann.
Beantworten wir also zuerst die Frage "Warum überhaupt MSIL?" Warum nicht einfach den C # -Compiler Maschinencode ausschreiben lassen?
Weil es billiger ist , es so zu machen. Angenommen, wir haben es nicht so gemacht. Angenommen, jede Sprache muss einen eigenen Maschinencodegenerator haben. Sie haben zwanzig verschiedene Sprachen: C #, JScript .NET , Visual Basic, IronPython , F # ... Und nehmen wir an, Sie haben zehn verschiedene Prozessoren. Wie viele Codegeneratoren müssen Sie schreiben? 20 x 10 = 200 Codegeneratoren. Das ist viel Arbeit. Angenommen, Sie möchten einen neuen Prozessor hinzufügen. Sie müssen den Codegenerator dafür zwanzig Mal schreiben, einen für jede Sprache.
Darüber hinaus ist es eine schwierige und gefährliche Arbeit. Das Schreiben effizienter Codegeneratoren für Chips, für die Sie kein Experte sind, ist eine schwierige Aufgabe! Compiler-Designer sind Experten für die semantische Analyse ihrer Sprache und nicht für die effiziente Registerzuweisung neuer Chipsätze.
Nehmen wir nun an, wir machen es auf CIL-Weise. Wie viele CIL-Generatoren müssen Sie schreiben? Eine pro Sprache. Wie viele JIT-Compiler müssen Sie schreiben? Eine pro Prozessor. Gesamt: 20 + 10 = 30 Codegeneratoren. Darüber hinaus ist der Language-to-CIL-Generator einfach zu schreiben, da CIL eine einfache Sprache ist, und der CIL-to-Machine-Code-Generator ist auch einfach zu schreiben, da CIL eine einfache Sprache ist. Wir werden alle Feinheiten von C # und VB los und so weiter und "senken" alles auf eine einfache Sprache, für die man leicht einen Jitter schreiben kann.
Eine Zwischensprache verfügt , senkt die Kosten , eine neue Sprache Compiler erzeugt dramatisch . Dies senkt auch die Kosten für die Unterstützung eines neuen Chips erheblich. Wenn Sie einen neuen Chip unterstützen möchten, finden Sie einige Experten auf diesem Chip und lassen sie einen CIL-Jitter schreiben, und Sie sind fertig. Sie unterstützen dann alle diese Sprachen auf Ihrem Chip.
OK, wir haben festgestellt, warum wir MSIL haben. weil eine Zwischensprache die Kosten senkt. Warum ist die Sprache dann eine "Stapelmaschine"?
Weil Stack-Maschinen für Sprachcompiler-Autoren konzeptionell sehr einfach zu handhaben sind. Stapel sind ein einfacher, leicht verständlicher Mechanismus zur Beschreibung von Berechnungen. Stapelmaschinen sind für JIT-Compiler-Autoren auch konzeptionell sehr einfach zu handhaben. Die Verwendung eines Stapels ist eine vereinfachende Abstraktion und senkt daher wiederum unsere Kosten .
Sie fragen: "Warum überhaupt einen Stapel?" Warum nicht einfach alles direkt aus dem Gedächtnis machen? Nun, lass uns darüber nachdenken. Angenommen, Sie möchten CIL-Code generieren für:
int x = A() + B() + C() + 10;
Angenommen, wir haben die Konvention, dass "add", "call", "store" usw. immer ihre Argumente vom Stapel nehmen und ihr Ergebnis (falls vorhanden) auf den Stapel legen. Um CIL-Code für dieses C # zu generieren, sagen wir einfach etwas wie:
load the address of x // The stack now contains address of x
call A() // The stack contains address of x and result of A()
call B() // Address of x, result of A(), result of B()
add // Address of x, result of A() + B()
call C() // Address of x, result of A() + B(), result of C()
add // Address of x, result of A() + B() + C()
load 10 // Address of x, result of A() + B() + C(), 10
add // Address of x, result of A() + B() + C() + 10
store in address // The result is now stored in x, and the stack is empty.
Nehmen wir nun an, wir hätten es ohne Stapel geschafft. Wir machen es auf Ihre Weise, wobei jeder Opcode die Adressen seiner Operanden und die Adresse, an die er sein Ergebnis speichert, übernimmt :
Allocate temporary store T1 for result of A()
Call A() with the address of T1
Allocate temporary store T2 for result of B()
Call B() with the address of T2
Allocate temporary store T3 for the result of the first addition
Add contents of T1 to T2, then store the result into the address of T3
Allocate temporary store T4 for the result of C()
Call C() with the address of T4
Allocate temporary store T5 for result of the second addition
...
Sie sehen, wie das geht? Unser Code wird riesig, weil wir explizit den gesamten temporären Speicher zuweisen müssen , der normalerweise nur auf dem Stapel liegt . Schlimmer noch, unsere Opcodes selbst werden alle enorm, weil sie jetzt alle die Adresse als Argument nehmen müssen, in die sie ihr Ergebnis schreiben werden, und die Adresse jedes Operanden. Eine "Add" -Anweisung, die weiß, dass zwei Dinge vom Stapel genommen und eine Sache angelegt werden, kann ein einzelnes Byte sein. Ein Add-Befehl, der zwei Operandenadressen und eine Ergebnisadresse benötigt, wird enorm sein.
Wir verwenden stapelbasierte Opcodes, da Stapel das häufig auftretende Problem lösen . Nämlich: Ich möchte einen temporären Speicher zuweisen, ihn sehr bald verwenden und ihn dann schnell entfernen, wenn ich fertig bin . Wenn wir davon ausgehen, dass wir einen Stapel zur Verfügung haben, können wir die Opcodes sehr klein und den Code sehr knapp machen.
UPDATE: Einige zusätzliche Gedanken
Im Übrigen ist diese Idee, die Kosten drastisch zu senken, indem (1) eine virtuelle Maschine angegeben, (2) Compiler geschrieben werden, die auf die VM-Sprache abzielen, und (3) Implementierungen der VM auf einer Vielzahl von Hardware geschrieben werden, überhaupt keine neue Idee . Es entstand nicht mit MSIL, LLVM, Java-Bytecode oder anderen modernen Infrastrukturen. Die früheste Umsetzung dieser Strategie, die mir bekannt ist, ist die Pcode-Maschine von 1966.
Das erste, was ich persönlich von diesem Konzept hörte, war, als ich erfuhr, wie die Infocom-Implementierer es geschafft haben, Zork auf so vielen verschiedenen Computern so gut zum Laufen zu bringen . Sie spezifizierten eine virtuelle Maschine namens Z-Maschine und erstellten dann Z-Maschinen-Emulatoren für die gesamte Hardware, auf der sie ihre Spiele ausführen wollten. Dies hatte den zusätzlichen enormen Vorteil, dass sie die Verwaltung des virtuellen Speichers auf primitiven 8-Bit-Systemen implementieren konnten. Ein Spiel könnte größer sein, als in den Speicher passen würde, da sie den Code einfach von der Festplatte einblättern könnten, wenn sie ihn benötigen, und ihn verwerfen könnten, wenn sie neuen Code laden müssten.