Stack-Cache statt Register

Gibt es einen Prozessor, der arithmetische Operationen auf einem Stapel und nicht auf Registern ausführt? Um die Leistung aufrechtzuerhalten, sollte dieser Prozessor natürlich den obersten Block eines Stapels in demselben Speichertyp zwischenspeichern, der für Register verwendet wird.

Ich habe in einem Artikel gelesen (David R. Ditzel, HR McLellan. Register Allocation for Free: The C Machine Stack Cache. ), dass ein Cache zweimal langsamer ist als Register, weil:

  • indirekte Adressierung bei jedem Zugriff auf den Cache;
  • Cache-Miss, wenn der Stack wächst.

Das Papier ist alt. Vielleicht sind Verbesserungen des Prozessordesigns aufgetaucht, die den Stack-Cache rentabel machen? Ich glaube, dass es die Komplexität von Compilern reduzieren und das Kopieren zwischen Registern und dem Rest des Speichers optimieren wird.

Aktualisierung 18.10.2012. Da dieses Konzept bekannt war (mir nicht), ändere ich die Frage in „… moderne Prozessoren?“

Aktualisierung 18.10.2012. Ich muss ausdrücklich sagen, dass ich nicht von einer „Null-Adress-Maschine“ spreche. Caching und „Nulladresse“ sind orthogonal. Mein hypothetischer Prozessor kann sogar eine 5-stellige Addition wie „r3 := r0+r2+r11+r5+r8“ haben. „rn“ bedeutet die Speicherzelle bei sp+n, wobei sp ein Stapelzeiger ist. sp ändert sich vor und nach einem Codeblock. Ein sehr ungewöhnliches Programm verändert sp bei jeder arithmetischen Operation.

Wie ich in meiner Antwort sagte, besteht eine grundlegende Schwierigkeit bei solchen Maschinen darin, dass es für die Befehlsplanungslogik im Allgemeinen schwierig ist, irgendeine Art von Kohärenz aufrechtzuerhalten, wenn sich der Stapelzeiger ändert. Nachdem dies gesagt wurde, kann ich mir vorstellen, dass es in einigen Fällen nützlich sein könnte, einen speziellen „Register-Save“-Stack für Register zu haben, die aufbewahrt werden müssen, auf die aber nur zugegriffen werden muss, um sie wiederherzustellen. Auf einem System mit 16 32-Bit-"Benutzer"-Registern könnte ein solcher Stack beispielsweise 16 tief und 512 Bit breit sein (plus einige Steuerbits).
Wenn eine Teilmenge von Registern gespeichert werden muss, werden alle 128 Bits der Registerdatei parallel auf den Stapel kopiert. wenn der Stack voll ist, würde der "Überlauf" als eine oder zwei Cachezeilen (abhängig von der Cachezeilengröße) in den Hauptcache geschrieben werden. Beim Wiederherstellen von Registern würden nur die für die Wiederherstellung vorgesehenen Register neu geladen. Eine solche Architektur könnte in vielen Fällen den Datenverkehr zum Speichern/Wiederherstellen von Registern zum und vom Hauptcache minimieren, aber ich bin mir nicht sicher, ob die Gesamtauswirkung auf die Leistung ausreichen würde, um dies zu rechtfertigen.
OK, da Sie nicht über Stapelmaschinen sprechen, habe ich das Papier, auf das Sie verweisen, aufgespürt und gelesen. Die Gründe, warum der Cache immer langsamer als die Register ist, geben sie anfangs an, unabhängig von der Implementierungstechnologie. Der explizit verwaltete Cache, den sie vorschlagen, liegt irgendwo dazwischen. In den 30 Jahren, seit dieser Artikel geschrieben wurde, ist die Compiler-Technologie viel ausgefeilter geworden und kann die Hardware, die für maximale Geschwindigkeit (unter Verwendung von Registern) gebaut wurde, voll ausnutzen.
@supercat: „Ich kann mir vorstellen, dass es in einigen Fällen nützlich sein könnte, einen speziellen „Register Save“-Stapel für Register zu haben, die aufbewahrt werden müssen.“ In einigen Fällen? Er-er. Dies ist der einzige Weg für rekursive Funktionen. ;)
@Dave Tweed: Ich habe deinen bezahlten Link entfernt; Der erste Link in den Google-Suchergebnissen ist ein kostenloser Download.
@Dave Tweed: Nun, die Compiler generieren Anweisungen zum Verschieben von Daten zwischen Stack und Registern. IMHO wäre dies automatisch schneller. Jedenfalls war das ursprüngliche Ziel, eine Spezifikation eines Prozessors kürzer zu machen.

Antworten (5)

Ja, die gesamte Linie der Mainframe-Computer von Burroughs , beginnend 1961 mit dem B5000, verwendete eine Stapelarchitektur.

In dieser Architektur ist die Verwaltung des Datenflusses zum und vom Stack eigentlich kein allzu großer Engpass für die Leistung. Ein größeres Problem ist die Tatsache, dass eine "Nulladressen"-Maschine viel mehr Anweisungen benötigt, um eine bestimmte Aufgabe zu erledigen, als eine Maschine mit einer, zwei oder drei Adressen. Die Befehlsdecodierung und die Ausführungspipeline werden zum primären Engpass.

Als ich dort in den frühen 1980er Jahren arbeitete, gab es Bestrebungen, eine CPU zu bauen, die relativ große Sequenzen von Anweisungen mit Nulladresse vorab abrufen und sie im laufenden Betrieb in Operationen mit drei Adressen übersetzen konnte, die in die Ausführungspipeline eingespeist wurden. (Denken Sie an einen in Hardware implementierten Java-JIT-Compiler.) Es wurde ziemlich komplex, insbesondere für die damals verfügbaren Implementierungstechnologien, und ich weiß nicht, ob diese Strategie letztendlich erfolgreich war.

Falls Sie sich fragen, bezieht sich die Terminologie "N-Adresse" auf die Anzahl der Operanden, die in einer einzelnen Anweisung angegeben werden können. Alle Operationen auf einer Stapelmaschine befinden sich implizit an den obersten ein oder zwei Stellen auf dem Stapel, daher gibt es in den Anweisungen null Operanden. Eine Maschine, die einen Akkumulator hat, der für alle Operationen in Verbindung mit einem anderen Register oder Speicherplatz verwendet wird, ist eine Maschine mit einer Adresse. Eine Maschine mit zwei Adressen kann einen beliebigen Quell- und Zieloperanden in einer Anweisung spezifizieren, und eine Maschine mit drei Adressen kann zwei Quelloperanden spezifizieren und das Ergebnis in ein unabhängiges Ziel setzen.

+1. Um die N-Adressierung in den heutigen Kontext zu stellen, haben die 8-Bit-PICs wie PIC 16 und PIC 18 hauptsächlich Anweisungen mit einer Adresse, da die meisten Operationen das W-Register für einen der Operanden implizieren und das Ergebnis entweder das W-Register oder zurück zu ist der Quellort. Der dsPIC und die Derivate (PIC 24, 30 und 33) sind größtenteils Maschinen mit 3 Adressen, obwohl die Operationen auf den Satz von 16 W-Registern beschränkt sind. Trotzdem können viele Operationen mit zwei W-Registern als Operanden durchgeführt und das Ergebnis in ein drittes geschrieben werden. Dies ist im Grunde die RISC-Version von 3-Adresse.
Wenn man eine bestimmte Anzahl von Bits in einem Opcode hat, um alle Adressen zu codieren, die Anweisungen benötigen, würde ich denken, dass der größere Arbeitssatz, der durch eine Architektur mit einer oder zwei Adressen ermöglicht wird, oft die Vorteile einer Architektur mit drei Adressen überwiegen würde. vorausgesetzt, dass der Befehlssatz die "Strafe" für die Fälle minimierte, in denen das Durchlaufen eines einzelnen Registers unzureichend war. Die Nulladresse funktioniert nicht sehr gut, aber ich würde denken, dass eine Stapelmaschine mit einer Adresse ziemlich gut sein könnte, wenn man nicht versuchen würde, Anweisungen zu aggressiv zu überlappen.
@OlinLathrop: Ich würde etwas Ähnliches wie die Einzeladressenanweisungen des PIC mit wählbarem Ziel als nahezu ideal betrachten, wenn die Eingabe "W" in die ALU stattdessen von einem Register käme, das normalerweise W widerspiegeln würde, außer nach einem "uselw" oder " usefw"-Anweisung (die sie mit einer Konstante oder dem Inhalt eines anderen Registers laden würde). Anstelle eines dedizierten „movff“-Opcodes aus zwei Wörtern würde ich die Sequenz „usefw src / movwf dest“ verwenden [danach würde das Temp-Register mit W neu geladen]. Das würde "usefw src / addwf dest,f" als Mittel von "dest += src" erlauben, ohne W zu stören.
@OlinLathrop: Für Anwendungen, bei denen alle häufig verwendeten Teile des Arbeitssatzes ohne Banking in den Adressbereich einer Anweisung passen können, movf src / addwf dest,fist schneller als ldr r0,[src+r13] / ldr r1,[dest+r13] / add r0,r0,r1 / str [src+r13](und führt seine Zielaktualisierung atomar durch). Schade, dass das Hinzufügen einer Zahl zu einer anderen, während der Wert von Wfür etwas anderes benötigt wird, vier Zyklen kostet (einen zum Speichern von W, einen zum Laden eines Operanden, einen zum Ausführen der Operation und einen zum Wiederherstellen von W). So etwas usefwkönnte das auf zwei reduzieren.
Nein, ich spreche nicht von einer „Nulladressen“-Maschine. Beispielsweise bedeutet der Operand „R5“ die Speicherzelle bei SP+5, und diese Speicherzelle wird zwischengespeichert, weil sie nahe an der Spitze des Stapels liegt.

Ich erinnere mich, dass ich vor etwa 17 Jahren einen ähnlichen Artikel (vielleicht denselben) gelesen habe. Ein solcher Ansatz könnte gut sein, wenn man einen Prozessor entwickeln würde, um schnell eine Anweisung nach der anderen auszuführen. Leider funktioniert es nicht gut mit der Out-of-Order-Befehlsplanung. Wenn man Code hat wie:

  ldr r1,[r0]
  ... irgendwas machen, ohne r1, r2 oder [r2] einzubeziehen
  str r1,[r2]

Einem Befehlsplaner steht es frei, diese beiden Befehle nach Belieben zu verschieben. Während es für den Befehlsplaner schwierig sein kann zu wissen, ob ein Schreibvorgang in einen Speicherort ein Schreibvorgang in [r2] sein könnte, erfordern viele kompilierte Sprachen, dass Programmierer angeben, welche Dinge Alias ​​sein können oder nicht.

Im Gegensatz dazu waren die Anweisungen eher wie folgt:

  mov.l [r0],[--sp] ; Schieben Sie [r0] auf den Stapel
  ... etwas tun, was sp betrifft
  mov.l [sp++],[r2] ; Pop [r2] aus dem Stapel

es wäre für eine Out-of-Order-Ausführungsmaschine viel schwieriger zu bestimmen, ob der Quelloperand für die letztere Anweisung immer derselbe wie der Zieloperand der ersteren wäre und ob irgendwelche dazwischenliegenden Anweisungen ihn beeinflussen könnten.

In der Vergangenheit habe ich mit dem Saab Ericsson Space Thor gearbeitet, einem Mikroprozessor für Weltraumanwendungen. Es funktionierte, hatte aber einige schwerwiegende Nachteile. Nur eins: Die Anweisungspipeline wurde offengelegt: Die Anweisung, die ein Wort aus dem Speicher geladen hat, wurde vor 2 Anweisungen als Adresse für die Spitze des Stapels verwendet . Ich habe eine schnelle Speicherkopierroutine dafür geschrieben, aber Saab sagte, sie könne nicht verwendet werden, weil Interrupts Probleme verursachen würden ...

Es gab dedizierte Forth-Prozessoren, die früher als Boot-Prozessor für Sun/Sparc-Maschinen verwendet wurden, deren dedizierte Architektur der Sprache zugeordnet war. Aber nicht allgemein verfügbar.

Der x86 ist fast einer davon :-) (und der x87 fp-Teil noch näher)

In modernen Systemen ist Stack jedoch schrecklich, da es über Kerne oder sogar NUMA-Knoten hinweg Alias ​​sein kann, sodass viele langsame Signalübertragungen über große Entfernungen beteiligt sein können. Oder zumindest mehr Verriegelungen als bei einer Registerdatei und einer Registerumbenennung.

Bedenken Sie, dass nicht einmal CPUs, sondern andere Geräte Daten in Ihren Stack DMA können – denken Sie an Lesepuffer!

Ja, fast. x86 hat AX, BX, CX, DX, BP, SI, DI. Diese Liste ist nicht besonders kurz. :) Tatsächlich habe ich Stack vs. Register auf AMD Athlon getestet und festgestellt, dass Register 2-mal schneller sind als Stack. DMA oder ein anderer Prozessor, der auf den Stack des Prozessors zugreift, ist normalerweise ein Programmierfehler, sodass der Prozessor diesen Konflikt nicht lösen muss, indem er in solchen Fällen sagt: „Verhalten ist undefiniert“.
Nein, DMA-Zugriff auf den Stack ist üblich - ziehen Sie Puffer auf dem Stack für Aufrufe von read() oder write() in Betracht. Dies ist kein Programmierfehler, und CPUs können dafür nicht "Verhalten undefiniert" sagen. Ich erinnere mich an ein altes PowerPC-Motherboard, bei dem dieses Verhalten aufgrund eines Fehlers in der Apple-Hardware nicht definiert war. es hat "Spaß" gemacht, damit umzugehen ... Der x87 ist ein vollständig stapelbasierter Befehlssatz, obwohl der "Arbeitsstapel" stark begrenzt ist und auf den "echten" Stapel übertragen werden muss.
„berücksichtigen Sie Puffer auf dem Stack für Aufrufe von read() oder write()“ Wir können das loswerden.
@JonWatte: Das Platzieren eines DMA-Puffers auf dem Stapel scheint eine schlechte Idee zu sein, wenn synchrone E / A verwendet wird, und eine wirklich sehr schlechte Idee für die Verwendung von asynchroner E / A. Selbst im Fall der synchronen E/A muss zumindest jede Multitasking-Executive wissen, wie sie alle anstehenden DMA-Operationen abbrechen kann, wenn sie einen Thread beenden muss. Und im Fall der asynchronen E/A ist es ein Rezept für eine Katastrophe, wenn die Routine, die den DMA einrichtet, unerwartet beendet wird, bevor der DMA abgeschlossen ist.
Offensichtlich kann asynchrone E/A keine Stapelpuffer verwenden. UNIX ist jedoch nicht besonders gut bei asynchroner E/A; Die meisten Programme verwenden tatsächlich synchrone E/A. Der Kernel muss nicht unbedingt auf den Abschluss der I/O warten, bevor er eine Stack-Zuordnung entfernt, solange die physischen Seiten noch einen Referenzzähler haben und nicht entfernt werden, bis die I/O abgeschlossen ist. Denken Sie daran: DMA wird normalerweise mit physischen Adressen außerhalb der VM-Übersetzungsschicht durchgeführt. Ich kenne Kernel, die auf physische Seiten verweisen; Ich weiß nicht, ob das alle tun.