Clause
← All dokumentation

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

Kontrakt för processheapen

Processheapar följer den klassiska ERTS-designen. Den här anteckningen är kontraktet; plan 11 phase C (steg 8A–8I) levererade det, och senare steg som nämns nedan utökar det. runtime.md sammanfattar API:t.

Vad phase C ersatte

Före phase CProblemErsättning
En lista av block (chunks) som aldrig flyttasCeller kan inte kompakteras eller kopieras; kapaciteten bara växerEtt sammanhängande heapblock plus fragment, flyttat av en kopierande skräpsamlare (8G, 8H)
Varje cell är en nod i ett std::map-index per processHeapord ensamma går inte att tolka; en värdallokering och en O(log n)-uppslagning per cellSjälvbeskrivande celler; tillträde via ägt intervall och huvud (8C, 8D)
Varje bitstring-cell har en fast 64-byte-array och en shared_ptr, frigjord via ett destruktorregisterStora celler för små data; ingenting kan flytta en cell eller hitta dess döda kopiorHeap-binaries av variabel storlek och binary-celler utanför heapen på en lista per process (8B)
Värdens Term låser heapen med shared_ptr<HeapStorage>; värden som runtime håller lever bara i Term-objektIngenting som en skräpsamlare kan hitta eller skriva omERTS-modellen: C++ håller råa ord endast mellan safepoints; värden som runtime håller är processens rotord; värdanropare skickar explicita rötter till collect() (8E)
En heapbuffert per genererad rotramIngen processtack att genomsökaEn stack av rotramar per process (8F)
Inget överflödesområdeAllokeringen ryms antingen i budgeten eller misslyckasHeapfragment medan heapen inte får flyttas (8G)

Genererad kod, dess ABI och varje observerbart programresultat förblir oförändrade.

Ordlayout

En term är ett målord (32 eller 64 bitar); kodningarna finns i abi.md. Varje heapområde är en sekvens av objekt som en genomlöpare tolkar från dess första ord:

Ingen cell behöver starkare justering än ett ord. memory/heap_walk tolkar ett område cell för cell och ProcessHeap::verify kontrollerar en hel heap (8C). Celler innehåller bara ord och byte, utom binary-cellen utanför heapens std::shared_ptr (nedan), så en cell flyttas genom att dess ord kopieras.

SortOrd efter huvudetSpårade ord
cons (inget huvud)2 ord totalthead, tail
tuplen elementplatseralla
map2n platser: nycklar i exakt termordning, var och en följd av sitt värdealla
native_recordadressen till runtimens RecordDefinition, sedan n fältvärden i definitionsordningvärden
fun_closureadressen till runtimens FunDefinition, sedan n fångade värden (funs)värden
bignumteckenord, sedan beloppets limbs, minst signifikant förstinga
floating8 byte: 1 ord (64 bitar) eller 2 ord (32 bitar)inga
reference8 byte: referensnumret (pids och referenser)inga
heap_binarybitlängd, sedan databyte avrundade uppåt till ord (högst 64 byte)inga
refc_binarybitoffset, bitlängd, std::shared_ptr (2 ord), länk för listan utanför heapen: 5 ordinga
fillern oanvända ordinga

Antalet för map anges i ord (poster = antal / 2). Pids är omedelbara värden som godtas mot de nummer runtime har utfärdat. Sorter som ännu inte godtas (externa identiteter) följer samma regler när de kommer: identiteter och deskriptorer är register-ID:n i ospårade ord, aldrig ägande C++-pekare.

Binaries utanför heapen

En binary större än 64 byte är en oföränderlig buffert som ligger utanför varje processheap och delas genom referensräkning (BEAM:s ProcBin och Binary).

Områden

Storlek och budget

Minnesgräns för runtime

Tillträde

Pekare in i en processheap skapas endast av kompilatorn och runtime inuti den processen och namnger alltid början av ett objekt; det finns inga inre pekare att upptäcka. Tillträde (8D) är en ägarskapskontroll för ord som lämnas tillbaka till en process:

  1. Adressen är ordjusterad inuti ett av processens områden, under dess top: heapblocket kontrolleras först, sedan fragmenten sorterade efter adress. Främmande och inaktuella ord misslyckas här utan någon läsning.
  2. Ett boxat ord namnger ett huvud av en godtagen sort (inte utfyllnad); ett listord namnger en cons-cell (ett ord som inte är ett huvud).

Åtkomstfunktioner avkodar sort, antal och nyttolast från själva huvudet. verify() förblir den fullständiga kontrollen av att varje plats namnger början av ett objekt, för tester.

Rötter och safepoints

ProcessContext::visit_roots räknar upp varje rotord för skräpsamlaren (step 23). Vid en safepoint håller ingenting annat heapord för processen:

ÄgareRotordAnteckningar
Ramens termplatserDe första roots platserna i varje ram på stacken (step 19)Bottenramar har inga; index för återupptagning och hanterare är heltal
Råa ramplatserIngaSpillda native-värden; genererad kod håller inget heapord där vid en safepoint (omläsningsregeln från step 24)
Registerx[0..live) (ProcessStack::keep_registers)En suspenderad ingångs argument (step 43); varje push och pop rensar live
FelkanalFelnyttolast (BEAM:s fvalue), argumentlistan till erlang:error/2,3, stackspårstermenBinds om på plats; fångade spårramar är deskriptorpekare in i koden
Trap-tillståndTermorden i en trappande inbyggd funktions TrapState (step 43A)Frigörs när den inbyggda funktionen avslutas eller misslyckas
BrevlådaVarje meddelande i signalinkorgen och meddelandekön (step 45), inklusive 'EXIT'- och 'DOWN'-meddelandenTills en receive tar det; skrivs om på plats, så receive-markören (en listposition) och tidsgränsen förblir giltiga
Explicita rötterDet intervall som en värd skickar till collect(roots) (8E)Läses tillbaka efter anropet
Lista utanför heapenIngaLänkar genomsöks och länkas om, de spåras inte

Ingen heapcell håller ett lås. Atomer är omedelbara värden och atomtabellen skräpsamlas aldrig. Fun-celler namnger kod via sin ospårade FunDefinition, som lever lika länge som runtime; inlästa moduler avlastas aldrig, så varken funs eller spårdeskriptorer behöver ett lås. Små omedelbara värden är inte rötter.

Som i ERTS C-kod är en Term hos värden ett rått taggat ord som är giltigt till nästa safepoint för dess heap. Den låser inte heaplagringen; den håller en svag livstidstoken för kontexten och heapens skräpsamlingsräknare, så användning efter nedmontering rapporterar expired_context och användning efter en senare skräpsamling rapporterar ett fel för inaktuell term. En Term är giltig endast inuti sin egen process; andra processer får bara läsa den.

Heapen flyttas endast vid en safepoint, och aldrig medan en reservation är öppen:

Varje annan begäran returnerar unsafe_point och ändrar ingenting, inte ens felkanalen för ett pågående genererat anrop. Allokering flyttar aldrig heapen: en begäran som inte ryms skapar ett fragment.

Skräpsamling i genererad kod

Beslut i plan 11 step 24 (2026-10-06), implementerat i step 26 (implementation). Genererad kod samlar skräp endast vid några få safepoints där varje levande term redan ligger i en rot. Allt annat, inklusive varje allokerande tjänst, är en kritisk sektion som aldrig flyttar heapen.

Utlösare

En safepoint samlar skräp när heapen begär det; annars kostar den en kontroll.

UtlösareVillkor vid safepointenMotsvarighet i ERTS
Full heapNågot fragment finns: en allokering rymdes inte i heapblocket sedan den senaste skräpsamlingenHeapens top når heapens slut
Tryck från binaries utanför heapenOrden utanför heapen når gränsen för den virtuella binary-heapen: 46 422 ord till en början, efter varje skräpsamling två gånger de överlevande orden utanför heapen, aldrig mindre än så, men högst de överlevande plus hälften av den budget som lämnas ledig efter heapblocket (step 27)bin_vheap_sz / virtuell binary-heap
erlang:garbage_collect/0Alltid; kommer med familjerna av inbyggda funktioner (steg 36-37) som en tvingad safepointExplicit fullständig genomsökning

Det nya blocket dimensioneras för de levande orden plus de stackord som används (ERTS håller stacken inuti heapblocket): en djup stack får en större heap, så en lång rekursion samlar skräp i proportion till sin allokering i stället för att genomsöka hela stacken med några hundra ords mellanrum.

Safepoints

PunktVarLevande utanför ramens termplatser
FunktionsingångI CLAUSE_enter_v1 / CLAUSE_tail_v1 (och därmed värdanrop), innan den anropades ram pushasDen anropades argument x[0..arity), behållna som rötter (keep_registers)
LoophuvudEtt anrop av CLAUSE_safepoint_v1(context) i huvudet på varje generatorloop i en comprehensionIngenting

Varje Erlang-loop är antingen rekursion, som passerar en funktionsingång per steg, eller en comprehension, som passerar sitt loophuvud, så skräpet mellan två safepoints begränsas av rak kod och enskilda tjänsteresultat.

Inte safepoints (kritiska sektioner, som fortsätter att allokera i fragment): varje annan runtime-tjänst, inklusive allokerings-, konstruktions- och matchningstjänster; CLAUSE_return_v1; propagering av undantag; och senare leverans av meddelanden (step 45). Tjänster kan därför hålla råa heapord i C++ under hela sin körning, och deras indataarrayer och utdata behöver ingen omläsning.

Väntande och suspenderade processer

Plan step 51. En process som inte körs skräpsamlas aldrig: den väntar i en receive, står i kö efter en yield eller trap, eller har inte startat än, och allt den håller är redan en rot (dess ramar, registren för den ingång eller fortsättning där den ska återupptas, trap-tillstånd och dess meddelanden). Meddelanden som skickas till den kopieras in i fragment av dess heap. Varje leverans väcker en väntande process, och att återuppta den upprepar ingången till dess fortsättning (den inbyggda vänta-funktionen, en trap-fortsättning eller den funktion där den gjorde yield), vilket är en safepoint vid funktionsingång: det första en återupptagen process gör är att samla skräp när dess heap begär det. En process som väntar i en selektiv receive som hoppar över många meddelanden samlar därför skräp allteftersom de anländer, på samma sätt som ERTS skräpsamlar en process nästa gång den schemaläggs. executables_mailbox_collection kontrollerar hamstring, väntan med tidsgräns och djup rekursion under meddelandelast, och att en konsument som kvitterar 3 000 meddelanden håller sig inom --max-heap 65536.

Förkastat: allokering som safepoint (BEAM:s test_heap). Det skulle kräva att varje tjänsteindata och varje SSA-term som är levande över en allokering låg i en rot, en omläsning efter varje allokerande tjänst och ett protokoll för nya försök i varje tjänst, medan de två safepoints ovan redan begränsar skräpet.

Omläsningsregel

Inget SSA-värde (ett värde i ett native-register) håller ett heapord över en safepoint, och ingen native-pekare korsar en överhuvudtaget (redan ett fel i lower_frames).

Beteende vid fel

Implementation

Step 26 (2026-10-06):

Step 27 (2026-10-06):

Step 27A (2026-10-06):

Prototyp

tests/prototypes/safepoint innehåller en loop i comprehension-stil i form efter lower_frames (loop.ll): en term Y som beräknas före loopen lagras i en termplats och läses om efter safepointen i loophuvudet. python tests/prototypes/safepoint/run.py kompilerar den för x86_64-pc-windows-msvc, aarch64-unknown-linux-gnu (64-bitarsord), i686-pc-windows-msvc och armv7-unknown-linux-gnueabihf (32-bitarsord) vid O0 och O2 och kontrollerar att en läsning av Y:s ramord följer efter safepoint-anropet. Alla åtta godkänns med clang 23.1.2. Vid O2 (i686) behåller loopen omläsningen från platsen och återanvänder aldrig registret som höll Y:

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

Skräpsamling

En Cheney-kopiering med fullständig genomsökning (8H, memory/heap_collect): allokera först det nya blocket (ett misslyckande ger out_of_memory och lämnar heapen orörd), markera sedan värdens Term-objekt som inaktuella, kopiera objektet bakom varje rotord och genomsök det nya blocket från vänster till höger medan barnen till varje kopia kopieras. Ett flyttat boxat objekts huvud ersätts av en boxad pekare till dess kopia; en flyttad cons-cell får ett nollställt head och en tail som pekar på dess kopia. Vidarebefordran bevarar delning. Rötter skrivs om på plats, listan utanför heapen genomsöks (kopior länkas om i listordning, döda celler förstörs), och det gamla blocket och fragmenten frigörs. En heap som aldrig allokerats skräpsamlas inte. CollectionStats rapporterar ord före, levande ord, det nya heapblocket, de sammanslagna fragmenten, stackens platskapacitet och ord utanför heapen.

Kopiering mellan heapar

ProcessHeap::add(value), likvärdigt med value.copy_to(heap), returnerar en term i målheapen (step 28, BEAM:s size_object och copy_struct):

Mätningar

runtime_heap_measurements (CTest i fullt läge; siffrorna skrivs ut, inga gränsvärden) bygger en lista med 100 000 element av {Index, Float}-tupler via TermFactory, går igenom den via kontrollerade åtkomstfunktioner och skapar 1 000 kontexter som var och en håller en liten tupel. Sedan 8I samlar den också skräp med listan som enda rot och går igenom kopian. Sidobyte är värdallokeringar utöver heapens underliggande lagring (ett objektindex före 8D, fragmentkedjan sedan 8G).

RevisionByggeKärnbygge / genomgångAnvända / kapacitet heapordSidobyteByte per kontextHeapord per kontext
bb09359 (blocklista, objektindex)Windows x64 Debug, clang-cl264 / 81 ms700 000 / 704 51224 002 256 (cirka 80 per cell)66 2178 192
8D (blocklista, ägt intervall)Windows x64 Debug, clang-cl185 / 147 ms700 000 / 704 5123 44066 0578 192
8G (heap på 233 ord, cirka 3 000 fragment)Windows x64 Debug, clang-cl219 / 174 ms700 000 / 706 223163 8782 377233
8I, före skräpsamlingWindows x64 Debug, clang-cl216 / 173 ms700 000 / 706 223163 8782 377233
8I, efter en skräpsamlingWindows x64 Debug, clang-clskräpsamling 56 ms / genomgång 72 ms700 000 / 999 6310——

Jämfört med baslinjen bb09359: metadata vid sidan av varje cell är borta (24 MB till ingenting efter skräpsamling), en kontext behöver 2,4 KB och 233 heapord i stället för 66 KB och 8 192 ord, byggandet är cirka 20 % snabbare, och genomgången är långsammare tills en skräpsamling slår ihop fragmenten (tillträde för fragment är en binärsökning över cirka 3 000 intervall); efter en sådan tar genomgången 72 ms. En levande mängd på 700 000 ord skräpsamlas på cirka 56 ms in i ett block på 999 631 ord, den ERTS-storlek som håller den under 75 %.