Clause
← Gesamte Dokumentation

Übersetzt aus dem englischen Original · 06042fa · 2026-10-09 · Auf Englisch lesen

Ausführungsmodell: Frames und Fortsetzungen

Entscheidung von Plan 11, step 17 (2026-10-05). Sie legt fest, wie erzeugte Erlang-Funktionen aufrufen, zurückkehren, sich unterbrechen und scheitern, sobald Rekursion, Endaufrufe und Prozesse hinzukommen. Step 19 hat Aufrufe, Rückkehr, Endaufrufe und Frames umgesetzt (Implementierung listet, was noch offen ist); die Steps 23, 24, 26 und 43 bauen darauf auf.

Entscheidung

Jeder Erlang-Prozess läuft auf seinem eigenen flachen Stack expliziter Frames, und erzeugter Code wechselt zwischen Funktionen nur durch garantierte Endübergaben (LLVM musttail). Ein Nicht-Endaufruf speichert die Fortsetzung (continuation) des Aufrufers in dessen Frame und springt zum Aufgerufenen; eine Rückkehr springt zu dieser Fortsetzung zurück. Der native Stack bleibt daher einen Aufruf tief über dem Scheduler, unabhängig von der Erlang-Rekursionstiefe, und ein Prozess kann bei jeder Übergabe anhalten und später auf einem beliebigen Thread fortfahren.

Prozesszustand

FeldBedeutung
stack, capacityEin wachsendes Wort-Array; wird beim Wachsen verschoben
frame, topWort-Offsets des aktuellen Frame-Headers und des ersten freien Worts
x[], liveArgument-/Ergebnisregister; die ersten live Wörter sind bei einer Übergabe Wurzeln
reductionsVerbleibende Aufrufe in der Zeitscheibe
resume_atEintritt, an dem ein unterbrochener Prozess fortfährt
FehlerkanalDer heutige Kanal der Revision 2: Grund, Nutzlast, Stacktrace, halted

Frames

Ein Frame ist ein fester Header, gefolgt von den Slots der Funktion (BEAM-Y-Register). Slots werden beim Push auf null gesetzt, wie heute Wurzel-Frames.

Header-WortBedeutung
previousOffset des Headers des Aufrufers (Frames verketten sich über Offsets, nie über Zeiger)
functionDer Deskriptor der Funktion
resumeFortsetzungsindex, über den der Rumpf verzweigt, wenn die Kontrolle hierher zurückkehrt
handlerFortsetzungsindex des innersten aktiven Handlers, 0 wenn keiner

Der Deskriptor erweitert das heutige abi::v1::FrameDescriptor (Moduldeskriptor, Slots für Modul- und Funktionsatom, Stelligkeit) um die Code-Zeiger für Eintritt und Rumpf sowie die Slot-Anzahl. Header-Wörter sind keine Terme; ein Walker folgt previous und liest Slot-Anzahlen aus Deskriptoren. Ein Boden-Frame (bottom frame) im Besitz der Runtime liegt unter dem ersten Aufruf jedes Prozesses: Fortsetzung 1 ist ein normales Ende (Ergebnis in x[0]), Fortsetzung 2 der Handler für eine nicht abgefangene Ausnahme.

Operationen

Eine Funktion, die keinen Nicht-Endaufruf macht und keinen Slot über einen Safepoint hinweg behält, darf ihren Frame auslassen und direkt zum Rumpf ihres Aufrufers zurückkehren. Das ist eine Optimierung, die der Compiler später hinzufügen kann, kein Teil des Vertrags.

Sichtbarkeit der Wurzeln

Bei jeder Übergabe und jedem Safepoint sind die Wurzeln eines Prozesses: alle Slots aller Frames auf seinem Stack, x[0..live) sowie Nutzlast, Argumentliste und Stack-Term des Fehlerkanals (bereits Prozesswurzeln). Die Wörter zur Ergebnisübergabe des heutigen Wurzel-Stacks entfallen; x[0] übernimmt ihre Rolle.

Werte überleben eine Übergabe nie in nativen Registern oder SSA-Werten. Jeder Rumpf lädt seine Frame-Adresse nach dem Betreten neu aus stack + frame und liest lebende Werte aus Slots. Innerhalb einer Fortsetzung macht ein Dienst, der einen Frame pushen oder den Heap verschieben kann, jeden in SSA-Werten gehaltenen Slot-Zeiger und Heap-Zeiger ungültig; die Neulade-Regel für Speicherbereinigungen ist in Speicherbereinigung in erzeugtem Code festgelegt.

Nachfolger des Wurzel-Stacks aus 8F

Der segmentierte Stack existiert nur, weil erzeugter Code absolute Frame-Zeiger über Aufrufe hinweg hält. In diesem Modell überlebt kein Frame-Zeiger eine Übergabe, daher wird der Stack zu einem flachen Block pro Prozess:

Zielplattformen

musttail mit der einheitlichen Signatur void (Process *) wird von jeder erforderlichen Zielplattform bei O0 und O2 akzeptiert (Prototyp unten, clang 23.1.2):

ZielWortErgebnis
x86_64-pc-windows-msvc, x86_64-unknown-linux-gnu64Endsprünge
i686-pc-windows-msvc, i686-unknown-linux-gnu32Endsprünge
aarch64-unknown-linux-gnu, arm64-apple-macosx14.064Endsprünge
armv7-unknown-linux-gnueabihf32Endsprünge

Das Backend meldet einen Fehler, wenn es einen musttail-Aufruf nicht einhalten kann, daher ist eine erfolgreiche Kompilierung die Garantie. Rückfallebene für eine künftige Zielplattform, die ihn ablehnt: ein Trampolin. Jeder Code gibt den nächsten Code-Zeiger an die Scheduler-Schleife zurück, statt zu springen (null unterbricht); Frames, Wurzeln und Fortsetzungsindizes bleiben unverändert. Keine erforderliche Zielplattform braucht es.

Implementierung

Step 19 (2026-10-05) setzt das Modell mit diesen Entscheidungen und Lücken um:

Verglichene Alternativen

Prototyp in tests/prototypes/execution_model: dieselben Erlang-Funktionen (sum/1 Rumpfrekursion, loop/2 Endrekursion, fail/1 löst boom in Tiefe N aus, catcher/1 fängt es ab) auf drei Arten von Hand abgesenkt. Host: Windows x64, clang 23.1.2; Zeiten sind Einzelläufe bei O2. Ausführen mit python tests/prototypes/execution_model/run.py.

Explizite Frames + musttail (gewählt)Native Aufrufe + Wurzel-Frames (heute)LLVM-Coroutinen (C++20)
Rumpfrekursion mit Tiefe 1Mok, 17 ms; 5 Wörter pro Frame; 17 Stack-Verschiebungen80 B nativer Stack pro Ebene: etwa 13.000 Ebenen in einem Thread mit 1 MiBok, 60 ms; eine Heap-Allokation von 64–80 B pro Aufruf
10M Endaufrufeok, 5 ms; konstanter StackO0 wächst um 80 B pro Aufruf; O2 nur durch Glück mit Sibling-Callskeine Endaufrufe: 1M Iterationen behalten 1M Frames
Abgabe / Fortsetzungjeder Eintritt; zwei Prozesse verschränken sichunmöglich ohne nativen Stack pro Prozesssymmetrische Übergabe (selbst musttail)
Nativer Stack bei Tiefe 1M136–144 Bwächst pro Ebene144–520 B
Für die GC sichtbare WurzelnSlots in bekannten FramesSlots in bekannten FramesCoroutinen-Frame-Layout von LLVM gewählt; Terme bräuchten eine zweite gewurzelte Kopie
AusnahmenAbwickeln bis zum Handler-FrameKanalprüfung pro RückkehrKanalprüfung pro Rückkehr

Die Trampolin-Variante des gewählten Modells besteht dieselben Läufe (20 ms Rekursion, 16 ms für 10M Endaufrufe bei O2). Verworfen:

Akzeptierte Kosten: ein indirekter Sprung pro Rückkehr plus ein Switch-Dispatch; über Aufrufe hinweg lebende Werte werden aus Slots neu geladen (dort sind sie ohnehin gespeichert); Backtraces nativer Debugger zeigen nur die aktuelle Funktion, während Erlang-Stacktraces aus der Frame-Kette stammen.

Belege aus dem Prototyp

run.py baut das gewählte Modell (musttail und Trampolin), die native Basislinie und das Coroutinen-Modell für den Host bei O0 und O2, führt sie aus und kompiliert die von Hand abgesenkten Funktionen (generated.cpp, freestanding) für jede obige Zielplattform bei O0 und O2, wobei es musttail-Aufrufe in der IR und Endsprünge in der Assembly zählt. Ergebnis vom 2026-10-05: PASS. Für das gewählte Modell auf beiden Stufen: Rückkehr (42), Rumpfrekursion mit Tiefe 1.000.000, 10.000.000 Endaufrufe, ein in Tiefe 100.000 ausgelöster Fehler, den ein Handler-Frame abfängt, derselbe Fehler nicht abgefangen (Boden-Frame, Trace aus 8 fail-Frames) und zwei Prozesse, die sich in Zeitscheiben zu 4.000 Reduktionen verschränken (1.002 Zeitscheiben). Alle 15 Übergabestellen sind bei O0 auf jeder Zielplattform musttail (14 bei O2 nach Inlining).

Der Rumpf von sum/1 für i686-pc-windows-msvc (O2-IR, Namen gekürzt). Die IR für x86_64-pc-windows-msvc ist dieselbe mit i64-Wörtern und doppelten Offsets.

%2 = load ptr, ptr %0, align 4                      ; stack base
%3 = getelementptr inbounds nuw i8, ptr %0, i32 8
%4 = load i32, ptr %3, align 4                      ; current frame offset
%5 = getelementptr inbounds nuw [4 x i8], ptr %2, i32 %4
%6 = getelementptr inbounds nuw i8, ptr %5, i32 16  ; slot 0 (N)
%7 = getelementptr inbounds nuw i8, ptr %5, i32 8   ; header: resume index
%8 = load i32, ptr %7, align 4
%9 = icmp eq i32 %8, 0                              ; switch on resume
...
16:                                                 ; N > 0: call sum(N - 1)
  store i32 1, ptr %7, align 4                      ; resume = 1
  ...                                               ; x0 = N - 1
  %19 = tail call ptr @call(ptr %0, ptr @SUM)       ; push frame or park
  musttail call void %19(ptr nonnull %0)
  ret void
20:                                                 ; resume 1: x0 += N
  ...
  %24 = tail call ptr @leave(ptr %0)                ; pop, caller body
  musttail call void %24(ptr nonnull %0)
  ret void