Clause
← Gesamte Dokumentation

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

Vertrag des Prozess-Heaps

Prozess-Heaps folgen dem klassischen ERTS-Design. Diese Notiz ist der Vertrag; Plan 11 phase C (steps 8A–8I) hat ihn umgesetzt, und spätere, unten genannte Schritte erweitern ihn. runtime.md fasst die API zusammen.

Was phase C ersetzt hat

Vor phase CProblemErsatz
Eine Liste von Chunks, die sich nie bewegenZellen können weder kompaktiert noch kopiert werden; die Kapazität wächst nurEin zusammenhängender Heap-Block plus Fragmente, verschoben durch eine kopierende Speicherbereinigung (8G, 8H)
Jede Zelle ist ein Knoten in einem prozesseigenen std::map-IndexHeap-Wörter allein sind nicht parsebar; eine Host-Allokation und ein O(log n)-Lookup pro ZelleSelbstbeschreibende Zellen; Zulassung über eigenen Bereich und Header (8C, 8D)
Jede bitstring-Zelle hat ein festes 64-Byte-Array und einen shared_ptr, freigegeben über eine Destruktor-RegistryGroße Zellen für kleine Daten; nichts kann eine Zelle verschieben oder ihre toten Kopien findenHeap-binaries variabler Größe und Off-Heap-binary-Zellen auf einer prozesseigenen Off-Heap-Liste (8B)
Ein Host-Term pinnt den Heap mit shared_ptr<HeapStorage>; von der Runtime gehaltene Werte leben nur in TermsNichts, was eine Speicherbereinigung finden oder umschreiben kannERTS-Modell: C++ hält rohe Wörter nur zwischen Safepoints; von der Runtime gehaltene Werte sind Wurzelwörter des Prozesses; Host-Aufrufer übergeben explizite Wurzeln an collect() (8E)
Ein Heap-Puffer pro erzeugtem Wurzel-FrameKein Prozess-Stack zum DurchsuchenEin Stack von Wurzel-Frames pro Prozess (8F)
Kein ÜberlaufbereichEine Allokation passt entweder ins Budget oder schlägt fehlHeap-Fragmente, solange sich der Heap nicht bewegen darf (8G)

Erzeugter Code, sein ABI und jedes beobachtbare Programmergebnis bleiben unverändert.

Wortlayout

Ein Term ist ein Zielwort (32 oder 64 Bits); die Kodierungen stehen in abi.md. Jeder Heap-Bereich ist eine Folge von Objekten, die ein Walker ab ihrem ersten Wort parst:

Keine Zelle braucht eine stärkere Ausrichtung als ein Wort. memory/heap_walk parst einen Bereich Zelle für Zelle, und ProcessHeap::verify prüft einen ganzen Heap (8C). Zellen enthalten nur Wörter und Bytes, außer dem std::shared_ptr des Off-Heap-binarys (unten), sodass eine Zelle durch Kopieren ihrer Wörter verschoben wird.

ArtWörter nach dem HeaderVerfolgte Wörter
cons (kein Header)insgesamt 2 WörterKopf, Tail
tuplen Elementplätzealle
map2n Plätze: Schlüssel in exakter Termordnung, jeweils gefolgt von ihrem Wertalle
native_recordAdresse der RecordDefinition der Runtime, dann n Feldwerte in DefinitionsreihenfolgeWerte
fun_closureAdresse der FunDefinition der Runtime, dann n erfasste Werte (Funs)Werte
bignumVorzeichenwort, dann Betragsglieder (limbs), niederwertigstes zuerstkeine
floating8 Bytes: 1 Wort (64 Bit) oder 2 Wörter (32 Bit)keine
reference8 Bytes: die Referenznummer (Pids und Referenzen)keine
heap_binaryBitlänge, dann Datenbytes, auf Wörter aufgerundet (höchstens 64 Bytes)keine
refc_binaryBit-Offset, Bitlänge, std::shared_ptr (2 Wörter), Off-Heap-Verknüpfung: 5 Wörterkeine
fillern ungenutzte Wörterkeine

Die Anzahl bei map ist in Wörtern angegeben (Einträge = Anzahl / 2). Pids sind Immediates und werden gegen die von der Runtime vergebenen Nummern zugelassen. Noch nicht zugelassene Arten (externe Identitäten) folgen denselben Regeln, sobald sie hinzukommen: Identitäten und Deskriptoren sind Registry-IDs in nicht verfolgten Wörtern, nie besitzende C++-Zeiger.

Off-Heap-binaries

Ein binary größer als 64 Bytes ist ein unveränderlicher Puffer, der außerhalb jedes Prozess-Heaps liegt und per Referenzzählung geteilt wird (BEAM ProcBin und Binary).

Bereiche

Dimensionierung und Budget

Runtime-Speicherlimit

Zulassung

Zeiger in einen Prozess-Heap werden nur vom Compiler und von der Runtime innerhalb dieses Prozesses erzeugt und benennen immer einen Objektanfang; es gibt keine inneren Zeiger zu erkennen. Die Zulassung (admission) (8D) ist eine Besitzprüfung für Wörter, die an einen Prozess zurückgegeben werden:

  1. Die Adresse ist wortausgerichtet innerhalb eines der Bereiche des Prozesses, unterhalb seines top: zuerst wird der Heap-Block geprüft, dann die nach Adresse sortierten Fragmente. Fremde und veraltete Wörter scheitern hier ohne jeden Ladezugriff.
  2. Ein geboxtes Wort benennt einen Header einer zugelassenen Art (keine Füllzelle); ein Listenwort benennt eine cons-Zelle (ein Wort, das kein Header ist).

Zugriffsfunktionen dekodieren Art, Anzahl und Nutzdaten aus dem Header selbst. verify() bleibt für Tests die vollständige Prüfung, dass jeder Platz einen Objektanfang benennt.

Wurzeln und Safepoints

ProcessContext::visit_roots zählt jedes Wurzelwort für die Speicherbereinigung auf (step 23). An einem Safepoint hält nichts anderes Heap-Wörter des Prozesses:

BesitzerWurzelwörterAnmerkungen
Termplätze der FramesDie ersten roots Plätze jedes Frames auf dem Stack (step 19)Unterste Frames haben keine; Fortsetzungs- und Handler-Indizes sind Ganzzahlen
Rohe Frame-PlätzeKeineAusgelagerte native Werte; erzeugter Code hält dort an einem Safepoint kein Heap-Wort (Neuladeregel von step 24)
Registerx[0..live) (ProcessStack::keep_registers)Die Argumente eines suspendierten Eintritts (step 43); jedes Push und Pop leert live
FehlerkanalFehler-Nutzdaten (BEAM fvalue), Argumentliste von erlang:error/2,3, Stacktrace-TermWerden an Ort und Stelle neu gebunden; erfasste Trace-Frames sind Deskriptorzeiger in den Code
Trap-ZustandDie Termwörter des TrapState eines trappenden Builtins (step 43A)Freigegeben, wenn das Builtin endet oder scheitert
MailboxJede Nachricht im Signal-Posteingang und in der Nachrichtenwarteschlange (step 45), einschließlich 'EXIT'- und 'DOWN'-NachrichtenBis ein receive sie entnimmt; an Ort und Stelle umgeschrieben, sodass der receive-Cursor (eine Listenposition) und die Timeout-Frist gültig bleiben
Explizite WurzelnDer Bereich, den ein Host an collect(roots) übergibt (8E)Nach dem Aufruf zurückgelesen
Off-Heap-ListeKeineVerknüpfungen werden durchlaufen und neu eingehängt, nicht verfolgt

Keine Heap-Zelle hält einen Pin. Atome sind Immediates, und die Atomtabelle wird nie bereinigt. Fun-Zellen benennen Code über ihre nicht verfolgte FunDefinition, die so lange lebt wie die Runtime; geladene Module werden nie entladen, daher brauchen weder funs noch Trace-Deskriptoren einen Pin. Kleine Immediates sind keine Wurzeln.

Wie im C-Code von ERTS ist ein Host-Term ein rohes getaggtes Wort, gültig bis zum nächsten Safepoint seines Heaps. Er pinnt keinen Heap-Speicher; er hält ein schwaches Lebensdauer-Token des Kontexts und den Bereinigungszähler des Heaps, sodass eine Verwendung nach dem Abbau expired_context meldet und eine Verwendung nach einer späteren Bereinigung einen Fehler wegen veraltetem Term. Ein Term ist nur innerhalb seines eigenen Prozesses gültig; andere Prozesse dürfen ihn nur lesen.

Der Heap bewegt sich nur an einem Safepoint und nie, während eine Reservierung offen ist:

Jede andere Anforderung liefert unsafe_point und ändert nichts, nicht einmal den Fehlerkanal eines laufenden erzeugten Aufrufs. Eine Allokation bewegt den Heap nie: eine Anforderung, die nicht passt, erzeugt ein Fragment.

Bereinigung in erzeugtem Code

Entscheidung von Plan 11 step 24 (2026-10-06), umgesetzt in step 26 (Implementierung). Erzeugter Code bereinigt nur an wenigen Safepoints, an denen jeder lebende Term bereits in einer Wurzel liegt. Alles andere, einschließlich jedes allozierenden Service, ist ein kritischer Abschnitt, der den Heap nie bewegt.

Auslöser

Ein Safepoint bereinigt, wenn der Heap danach verlangt; andernfalls kostet er eine Prüfung.

AuslöserBedingung am SafepointGegenstück in ERTS
Heap vollEs existiert ein Fragment: seit der letzten Bereinigung passte eine Allokation nicht in den Heap-BlockHeap-Top erreicht das Heap-Ende
Druck durch Off-Heap-binariesOff-Heap-Wörter erreichen das Limit des virtuellen Binary-Heaps: anfangs 46.422 Wörter, nach jeder Bereinigung das Doppelte der überlebenden Off-Heap-Wörter, nie weniger als dieses, aber höchstens die Überlebenden plus die Hälfte des nach dem Heap-Block frei verbleibenden Budgets (step 27)bin_vheap_sz / virtueller Binary-Heap
erlang:garbage_collect/0Immer; kommt mit den Builtin-Familien (steps 36-37) als erzwungener SafepointExpliziter vollständiger Durchlauf

Der neue Block wird für die lebenden Wörter plus die benutzten Stack-Wörter dimensioniert (ERTS hält den Stack innerhalb des Heap-Blocks): ein tiefer Stack bekommt einen größeren Heap, sodass eine lange Rekursion proportional zu ihrer Allokation bereinigt, statt alle paar hundert Wörter den ganzen Stack erneut zu durchsuchen.

Safepoints

PunktWoLebend außerhalb der Termplätze der Frames
FunktionseintrittIn CLAUSE_enter_v1 / CLAUSE_tail_v1 (und damit beim Host-Aufruf), bevor der Frame des Aufgerufenen abgelegt wirdDie Argumente des Aufgerufenen x[0..arity), als Wurzeln gehalten (keep_registers)
SchleifenkopfEin Aufruf von CLAUSE_safepoint_v1(context) am Kopf jeder Generatorschleife einer comprehensionNichts

Jede Erlang-Schleife ist entweder eine Rekursion, die pro Schritt einen Funktionseintritt passiert, oder eine comprehension, die ihren Schleifenkopf passiert, sodass der Müll zwischen zwei Safepoints durch geradlinigen Code und einzelne Service-Ergebnisse begrenzt ist.

Keine Safepoints (kritische Abschnitte, die weiter in Fragmente allozieren): jeder andere Runtime-Service, einschließlich Allokations-, Konstruktions- und Matching-Services; CLAUSE_return_v1; Ausnahmeweitergabe; und später die Nachrichtenzustellung (step 45). Services dürfen daher während ihres gesamten Laufs rohe Heap-Wörter in C++ halten, und ihre Eingabearrays und Ausgaben müssen nicht neu geladen werden.

Wartende und suspendierte Prozesse

Plan step 51. Ein Prozess, der nicht läuft, wird nie bereinigt: er wartet in einem receive, ist nach einem Yield oder Trap eingereiht oder noch nicht gestartet, und alles, was er hält, ist bereits eine Wurzel (seine Frames, die Register des Eintritts oder der Fortsetzung, an dem bzw. der er fortfährt, der Trap-Zustand und seine Nachrichten). An ihn gesendete Nachrichten werden in Fragmente seines Heaps kopiert. Jede Zustellung weckt einen wartenden Prozess, und seine Fortsetzung wiederholt den Eintritt seiner Fortsetzung (das Warte-Builtin, eine Trap-Fortsetzung oder die Funktion, an der er per Yield abgegeben hat), was ein Safepoint beim Funktionseintritt ist: das Erste, was ein fortgesetzter Prozess tut, ist zu bereinigen, wenn sein Heap danach verlangt. Ein Prozess, der in einem selektiven receive wartet, das viele Nachrichten überspringt, bereinigt daher, während sie eintreffen, so wie ERTS einen Prozess bereinigt, wenn er das nächste Mal eingeplant wird. executables_mailbox_collection prüft Horten, Warten mit Timeout und tiefe Rekursion unter Nachrichtenlast sowie, dass ein Konsument, der 3.000 Nachrichten bestätigt, innerhalb von --max-heap 65536 bleibt.

Verworfen: Allokation als Safepoint (BEAM test_heap). Sie würde erfordern, dass jede Service-Eingabe und jeder über eine Allokation hinweg lebende SSA-Term in einer Wurzel liegt, ein Neuladen nach jedem allozierenden Service und ein Wiederholungsprotokoll in jedem Service, während die beiden obigen Safepoints den Müll bereits begrenzen.

Neuladeregel

Kein SSA-Wert (ein Wert in einem nativen Register) hält ein Heap-Wort über einen Safepoint hinweg, und kein nativer Zeiger überquert überhaupt einen (bereits ein Fehler in lower_frames).

Fehlerverhalten

Implementierung

Step 26 (2026-10-06):

Step 27 (2026-10-06):

Step 27A (2026-10-06):

Prototyp

tests/prototypes/safepoint enthält eine Schleife im Stil einer comprehension in der Form nach lower_frames (loop.ll): ein vor der Schleife berechneter Term Y wird in einen Termplatz gespeichert und nach dem Safepoint am Schleifenkopf neu geladen. python tests/prototypes/safepoint/run.py kompiliert ihn für x86_64-pc-windows-msvc, aarch64-unknown-linux-gnu (64-Bit-Wörter), i686-pc-windows-msvc und armv7-unknown-linux-gnueabihf (32-Bit-Wörter) bei O0 und O2 und prüft, dass auf den Safepoint-Aufruf ein Ladezugriff auf das Frame-Wort von Y folgt. Alle acht bestehen mit clang 23.1.2. Bei O2 (i686) behält die Schleife das Neuladen aus dem Platz bei und verwendet nie das Register wieder, das Y enthielt:

LBB0_2:                     # loop head
    pushl  %edi
    calll  _clause_safepoint_v1
    pushl  20(%esi)         # cursor reloaded from its term slot
    ...
    pushl  24(%esi)         # accumulator
    pushl  28(%esi)         # Y reloaded from its term slot
    calll  _make

Speicherbereinigung

Eine Cheney-Kopie mit vollständigem Durchlauf (8H, memory/heap_collect): zuerst wird der neue Block alloziert (ein Fehlschlag ist out_of_memory und lässt den Heap unberührt), dann werden Host-Terms als veraltet markiert, das Objekt hinter jedem Wurzelwort kopiert und der neue Block von links nach rechts durchsucht, wobei die Kinder jeder Kopie kopiert werden. Der Header eines verschobenen geboxten Objekts wird durch einen geboxten Zeiger auf seine Kopie ersetzt; eine verschobene cons-Zelle bekommt einen Null-Kopf und einen Tail, der auf ihre Kopie zeigt. Die Weiterleitung erhält geteilte Strukturen. Wurzeln werden an Ort und Stelle umgeschrieben, die Off-Heap-Liste wird durchlaufen (Kopien in Listenreihenfolge neu eingehängt, tote Zellen zerstört), und der alte Block und die Fragmente werden freigegeben. Ein Heap, der nie alloziert wurde, wird nicht bereinigt. CollectionStats meldet die Wörter davor, die lebenden Wörter, den neuen Heap-Block, die zusammengeführten Fragmente, die Kapazität der Stack-Plätze und die Off-Heap-Wörter.

Kopieren zwischen Heaps

ProcessHeap::add(value), gleichbedeutend value.copy_to(heap), liefert einen Term des Ziel-Heaps (step 28, BEAM size_object und copy_struct):

Messungen

runtime_heap_measurements (CTest im Vollmodus; Zahlen werden ausgegeben, nicht geprüft) baut über TermFactory eine Liste von 100.000 Elementen aus {Index, Float}-Tupeln, durchläuft sie über geprüfte Zugriffsfunktionen und erzeugt 1.000 Kontexte, die je ein kleines Tupel halten. Seit 8I bereinigt er außerdem mit der Liste als einziger Wurzel und durchläuft die Kopie. Nebenbytes sind Host-Allokationen über den Heap-Speicher hinaus (vor 8D ein Objektindex, seit 8G die Fragmentkette).

RevisionBuildKernel-Aufbau / DurchlaufHeap belegt / Kapazität in WörternNebenbytesBytes pro KontextHeap-Wörter pro Kontext
bb09359 (Chunk-Liste, Objektindex)Windows x64 Debug, clang-cl264 / 81 ms700.000 / 704.51224.002.256 (etwa 80 pro Zelle)66.2178.192
8D (Chunk-Liste, eigener Bereich)Windows x64 Debug, clang-cl185 / 147 ms700.000 / 704.5123.44066.0578.192
8G (Heap mit 233 Wörtern, etwa 3.000 Fragmente)Windows x64 Debug, clang-cl219 / 174 ms700.000 / 706.223163.8782.377233
8I, vor der BereinigungWindows x64 Debug, clang-cl216 / 173 ms700.000 / 706.223163.8782.377233
8I, nach einer BereinigungWindows x64 Debug, clang-clBereinigung 56 ms / Durchlauf 72 ms700.000 / 999.6310——

Gegenüber der Basis bb09359: die Nebenmetadaten pro Zelle sind verschwunden (von 24 MB auf null nach einer Bereinigung), ein Kontext braucht 2,4 KB und 233 Heap-Wörter statt 66 KB und 8.192 Wörtern, der Aufbau ist etwa 20 % schneller, und der Durchlauf ist langsamer, bis eine Bereinigung die Fragmente zusammenführt (die Fragmentzulassung ist eine binäre Suche über etwa 3.000 Bereiche); danach dauert der Durchlauf 72 ms. Eine lebende Menge von 700.000 Wörtern wird in etwa 56 ms in einen Block von 999.631 Wörtern bereinigt, die ERTS-Größe, die sie unter 75 % hält.