Clause
← All dokumentation

Översatt från det engelska originalet · 06042fa · 2026-10-09 · Läs på engelska

Exekveringsmodell: ramar och fortsättningar

Beslut i plan 11 step 17 (2026-10-05). Det fastställer hur genererade Erlang-funktioner anropar, returnerar, suspenderas och misslyckas när rekursion, svansanrop och processer tillkommer. Step 19 implementerade anrop, returer, svansanrop och ramar (Implementation listar vad som fortfarande är öppet); steps 23, 24, 26 och 43 bygger på det.

Beslut

Varje Erlang-process körs på sin egen platta stack av explicita ramar, och genererad kod rör sig mellan funktioner endast genom garanterade svansöverföringar (LLVM musttail). Ett anrop som inte är ett svansanrop lagrar anroparens fortsättning (continuation) i anroparens ram och hoppar till den anropade; en retur hoppar tillbaka till den fortsättningen. Den inbyggda (native) stacken förblir därför ett anrop djup ovanför schemaläggaren, oavsett Erlang-rekursionens djup, och en process kan stanna vid vilken överföring som helst och återupptas senare på vilken tråd som helst.

Processtillstånd

FältBetydelse
stack, capacityEn växande ordarray; flyttas när den växer
frame, topOrdoffset för det aktuella ramhuvudet och för det första lediga ordet
x[], liveArgument-/resultatregister; de första live orden är rötter vid en överföring
reductionsÅterstående anrop i tidsskivan
resume_atIngången där en suspenderad process fortsätter
felkanalDagens kanal av revision 2: orsak, nyttolast, stackspårning, halted

Ramar

En ram är ett fast huvud följt av funktionens platser (BEAM:s Y-register). Platserna nollställs vid push, som rotramar görs i dag.

HuvudordBetydelse
previousOffset för anroparens huvud (ramar länkas med offset, aldrig pekare)
functionFunktionens deskriptor
resumeFortsättningsindex som kroppen väljer på när kontrollen återvänder hit
handlerFortsättningsindex för den innersta aktiva hanteraren, 0 när ingen finns

Deskriptorn utökar dagens abi::v1::FrameDescriptor (moduldeskriptor, platser för modul- och funktionsatomer, aritet) med kodpekarna för ingång och kropp samt antalet platser. Huvudord är inte termer; en genomlöpare följer previous och läser antalet platser från deskriptorer. En bottenram som ägs av runtimen ligger under det första anropet i varje process: fortsättning 1 är ett normalt avslut (resultatet i x[0]), fortsättning 2 hanteraren för ett ofångat undantag.

Operationer

En funktion som inte gör något anrop som inte är ett svansanrop och inte behåller någon plats över en safepoint kan hoppa över sin ram och returnera direkt till anroparens kropp. Detta är en optimering som kompilatorn kan lägga till senare, inte en del av kontraktet.

Synlighet för rötter

Vid varje överföring och varje safepoint är en process rötter: alla platser i alla ramar på dess stack, x[0..live) samt felkanalens nyttolast, argumentlista och stackterm (redan processrötter). Överlämningsorden för resultat i dagens rotstack försvinner; x[0] tar över deras roll.

Värden överlever aldrig en överföring i native-register eller SSA-värden. Varje kropp laddar om sin ramadress från stack + frame efter att den har anropats och läser levande värden från platser. Inom en fortsättning gör en tjänst som kan pusha en ram eller flytta heapen varje platspekare och heappekare som hålls i SSA-värden ogiltig; omladdningsregeln för skräpsamlingar fastställs i skräpsamling i genererad kod.

Efterföljare till rotstacken från 8F

Den segmenterade stacken finns bara för att genererad kod håller absoluta rampekare över anrop. Under denna modell överlever ingen rampekare en överföring, så stacken blir ett platt block per process:

Mål

musttail med den enhetliga signaturen void (Process *) accepteras av varje obligatoriskt mål vid O0 och O2 (prototyp nedan, clang 23.1.2):

MålOrdResultat
x86_64-pc-windows-msvc, x86_64-unknown-linux-gnu64svanshopp
i686-pc-windows-msvc, i686-unknown-linux-gnu32svanshopp
aarch64-unknown-linux-gnu, arm64-apple-macosx14.064svanshopp
armv7-unknown-linux-gnueabihf32svanshopp

Backend rapporterar ett fel när den inte kan uppfylla ett musttail-anrop, så en lyckad kompilering är garantin. Reserv för ett framtida mål som avvisar det: en studsmatta (trampoline). Varje kodstycke returnerar nästa kodpekare till schemaläggarloopen i stället för att hoppa (null suspenderar); ramar, rötter och fortsättningsindex är oförändrade. Inget obligatoriskt mål behöver den.

Implementation

Step 19 (2026-10-05) implementerar modellen med dessa val och luckor:

Jämförda alternativ

Prototyp i tests/prototypes/execution_model: samma Erlang-funktioner (sum/1 kroppsrekursion, loop/2 svansrekursion, fail/1 som kastar boom på djup N, catcher/1 som fångar det) sänkta för hand på tre sätt. Värd: Windows x64, clang 23.1.2; tiderna är enstaka körningar vid O2. Kör python tests/prototypes/execution_model/run.py.

Explicita ramar + musttail (valt)Native-anrop + rotramar (i dag)LLVM-korutiner (C++20)
Kroppsrekursion 1M djupok, 17 ms; 5 ord per ram; 17 stackflyttar80 B native-stack per nivå: omkring 13 000 nivåer i en tråd på 1 MiBok, 60 ms; en allokering på 64–80 B på heapen per anrop
10M svansanropok, 5 ms; konstant stackO0 växer 80 B per anrop; O2 endast genom tur med syskonanropinga svansanrop: 1M iterationer behåller 1M ramar
Lämna ifrån sig / återupptavarje ingång; två processer växlaromöjligt utan en native-stack per processsymmetrisk överföring (själv musttail)
Native-stack på djup 1M136–144 Bväxer per nivå144–520 B
Rötter synliga för GCplatser i kända ramarplatser i kända ramarkorutinens ramlayout väljs av LLVM; termer skulle behöva en andra rotad kopia
Undantagrullas upp till hanterarramenkanalkontroll per returkanalkontroll per retur

Studsmattevarianten av den valda modellen klarar samma körningar (20 ms rekursion, 16 ms för 10M svansanrop vid O2). Avvisade:

Accepterade kostnader: ett indirekt hopp per retur plus en switch-dispatch; värden som lever över anrop laddas om från platser (de lagras redan där); bakåtspårningar i native-debuggern visar endast den aktuella funktionen, medan Erlang-stackspårningar kommer från ramkedjan.

Belägg från prototypen

run.py bygger den valda modellen (musttail och studsmatta), native-baslinjen och korutinmodellen för värden vid O0 och O2, kör dem och kompilerar de handsänkta funktionerna (generated.cpp, fristående) för varje mål ovan vid O0 och O2, och räknar musttail-anrop i IR och svanshopp i assemblern. Resultat 2026-10-05: PASS. För den valda modellen på båda nivåerna: retur (42), kroppsrekursion med djup 1 000 000, 10 000 000 svansanrop, ett fel som kastas på djup 100 000 och fångas av en hanterarram, samma fel ofångat (bottenram, spårning av 8 fail-ramar), och två processer som växlar i tidsskivor om 4 000 reduktioner (1 002 tidsskivor). Alla 15 överföringsplatser är musttail vid O0 på varje mål (14 vid O2 efter inlining).

Kroppen för sum/1 för i686-pc-windows-msvc (O2 IR, förkortade namn). IR för x86_64-pc-windows-msvc är densamma med i64-ord och fördubblade offset.

%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