Clause
← All dokumentation

Översatt från det engelska originalet · 1e3a39c8-dirty · 2026-10-26 · Läs på engelska

Kontrakt för processheapen

Processheapar följer den klassiska ERTS-designen: ett sammanhängande heapblock per process plus överflödesfragment, självbeskrivande celler, stora binaries i delade buffertar utanför heapen, och en kopierande skräpsamlare som körs endast vid säkra punkter (safe points). Den här anteckningen är kontraktet; runtime.md sammanfattar värdens API.

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. Celler innehåller bara ord och byte, utom binary-cellens std::shared_ptr för data utanför heapen (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ärde; högst 32 posteralla
tree_mapantal poster, redigeringsord, rotens map_node: 3 ord (map-träd)roten
map_nodeantal poster under, redigeringsord, sedan n nycklar och n värden (ett löv) eller n barn (en gren); aldrig en termnycklar och värden eller barn
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, buffertens första byte (läses av genererad kod), slut och gräns för tillägg, std::shared_ptr (2 ord), länk för listan utanför heapen: 8 ordinga
sub_binarybitoffset in i originalet, bitlängd, originalets heap_binary eller refc_binary (aldrig en sub-binary): 3 ordoriginalet
fillern oanvända ordinga

Antalet för map anges i ord (poster = antal / 2). Pids och portar är omedelbara värden som godtas mot de nummer runtime har utfärdat.

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).

Återanvändning av buffertar

En frigjord buffert vars storlek är en tvåpotens på minst 256 byte (kapaciteterna hos binaries som kan utökas) stannar hos tråden som släppte dess sista referens, för den trådens nästa buffert av exakt samma storlek (som heapblock). Varje tråd behåller högst 32 buffertar och 8 MB; cachen frigörs när tråden avslutas. En återanvänd bufferts byte efter den nya binaryns egna lämnas som de var: ingen vy täcker dem. Utan detta tog loopar som utökar binaries nya sidor för varje utökad buffert (C++-heapen lämnar tillbaka sidorna i stora frigjorda block). Nya buffertar är ByteBuffers (runtime/include/byte_buffer.hpp), som inte heller nollställer sitt utrymme, så en portdrivrutins svar (en hel filläsning) skrivs en gång och tas över som det är.

Handtagsreferenser

En handtagsreferens är en referens som också håller en buffert utanför heapen, så som en magisk referens i OTP håller en resurs; atomics-arrayer lever i dem (atomics och counters).

Map-träd

En map med fler än 32 poster (OTP:s MAP_SMALL_MAP_LIMIT) är en tree_map-stubbe på 4 ord som namnger roten i ett persistent B+-träd (terms/map_tree) vars noder är map_node-celler i samma heap, så som OTP håller sina HAMT-noder på heapen. Mindre maps förblir map-celler.

Områden

Storlek och budget

Heapblock

Heapblock, fragment och gamla områden kommer från memory/heap_blocks. Ett block som en skräpsamling frigör stannar hos sin tråd för trådens nästa skräpsamlingar (så som ERTS-bärare (carriers) behåller minne): ett nytt block tar det minsta cachade blocket som är minst lika stort och högst dubbelt så stort, annars nytt minne. Varje tråd behåller högst 8 block och 16M ord (128 MB på 64-bitarsmål); block under 4 096 ord behålls inte, eftersom C++-heapens fria listor betjänar dem billigt. Ett frigjort block som inte ryms frigör först de äldsta behållna blocken: en växande heap frigör allt större block, och utan utkastning fyllde blocken från dess tidiga, små skräpsamlingar cachen, så att en levande mängd på miljontals ord tog nya sidor för varje ungt block, gammalt område och fullständig genomsökning. Utan detta tog varje skräpsamlings målutrymme (to-space) nya sidor från operativsystemet och betalade ett sidfel per sida det rörde, och att frigöra ett stort block lämnade tillbaka dess sidor. Cachade block debiteras inte någon process eller det runtime-omfattande kontot; en tråd frigör sin cache när den avslutas.

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 (admission) är en ägarskapskontroll för ord som lämnas tillbaka till en process. Term::from_word(word, context) / TermAccess::admit kontrollerar först att lagringen lever (expired_context); atomer och pids/portar måste tillhöra denna runtime (fasta atomer delas). För sammansatta ord:

  1. Adressen är ordjusterad inuti ett av processens områden, under dess top: heapblocket kontrolleras först, sedan fragmenten sorterade efter adress, sedan literalområdena (områden). Adresser utanför dessa intervall misslyckas utan någon läsning; detta upptäcker inte gamla ord på återanvända adresser.
  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).

Efter tillträde avkodar TermAccess::object sort, antal och nyttolast via trusted_cell, utan heapuppslag; child omsluter det lagrade ordet och validate kontrollerar endast representationen. Fabriker prövar fortfarande indata igen via ProcessHeap::retain och avvisar främmande sammansatta indata med wrong_owner.

Dessa kontroller körs endast i Debug-byggen (CHECKED_ADMISSION). Ett Release-bygge litar på varje sammansatt ord, atom-, pid- och portord som lämnas till en inbyggd funktion, tjänst eller fabrik, som ERTS litar på sina Eterms: genererad kod producerar endast ord från den egna processen, literalområden och runtimens atomer, så intervall- och ägaruppslagen gav bara en kostnad för varje argument. Andra omedelbara värden klassificeras ännu. Tester som matar in förfalskade eller främmande ord väntar sig avvisning endast när CHECKED_ADMISSION gäller. map_find får jämföra en annan process nyckel skrivskyddat utan avvisningen. verify() förblir den fullständiga kontrollen av att varje plats namnger början av ett objekt, för tester.

Rötter och säkra punkter

ProcessContext::visit_roots räknar upp varje rotord för skräpsamlaren. Vid en säker punkt håller ingenting annat heapord för processen:

ÄgareRotordAnteckningar
Ramens termplatserDe första roots platserna i varje ram på stackenBottenramar 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 säker punkt (omläsningsregel)
Registerx[0..live) (ProcessStack::keep_registers)En suspenderad ingångs argument; varje push och pop rensar live
FelkanalFelnyttolast (BEAM:s fvalue), argumentlista för erlang:error/2,3, stackspårstermGeneratedCallState Terms skrivs om av Term::rebind, som endast lagrar det nya ordet; fångade spårramar är deskriptorpekare in i kod
Trap-tillståndTermorden i en trappande inbyggd funktions TrapState (portioner)Frigörs när den inbyggda funktionen avslutas eller misslyckas
ProcesslexikonVarje nyckel och värdeTills de raderas eller processen avslutas
BrevlådaVarje meddelande i signalinkorgen och meddelandekön, 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)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 värd-Term exakt ett trivialt kopierbart ord, giltigt till heapens nästa skräpsamling eller processens slut; den har inget fäste, ingen heappekare, livstidstoken eller skräpsamlingsepok (ägarskap). Åtkomst efter någon av dessa gränser läser flyttat eller frigjort minne och är odefinierad, utan stale_term-kontroll. Registrerade rötter skrivs om; en oregistrerad värd-Term måste byggas om från sitt omskrivna rotord. En Term tillhör sin process; andra processer får bara läsa den medan källan förblir stabil.

Heapen flyttas endast vid en säker punkt, 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.

Meddelandebuffertar

En MessageBuffer (message_buffer.hpp) håller en term utanför varje process: copy_of(value, sender) kopierar grafen från avsändarens processheap till en egen HeapStorage. Avsändarens ProcessContext ger heapalternativ, literalområden, atomer, identiteter och runtimens minneskonto; bufferten behåller en egen livstidstoken och delar binaries utanför heapen. Den använder samma GraphCopy som en sändning; copy_to kopierar den till en mottagares heap. Omedelbara värden och literalrötter behöver ingen lagring. Fabriker, reservationer och meddelandebuffertar behåller sina livstidskontroller (expired_context); en Term läst från en buffert fäster inget och får inte överleva sitt underlag. Bufferten skräpsamlas aldrig och frigörs med sin sista ägare; dess block är exakt termens ord, oavsett avsändarens minsta heap. Sändningar till andra processer (ankomstbuffertar), timermeddelanden och monitortaggar använder den, så de kodas aldrig om; persistenta termer lever i ett eget adressintervall och kopieras till buffertar i stället för att delas.

En enda meddelandegenomgång. En sändning till en annan process går igenom meddelandets graf en gång, före varje lås (OutgoingMessage, med GraphCopy från graph_copy.hpp); genomgången dimensionerar varje senare kopia och kopierar, liksom varje kopia utanför en process, persistenta termer.

Överlämning av meddelanden. En sändning vars meddelande tar minst MessageBuffer::ADOPTED_WORDS (512) ord går alltid via en buffert, kopierad utifrån den genomgången före varje lås, så att avsändaren inte håller något lås under kopieringen. Mottagaren tar sedan över bufferten i stället för att kopiera den (MessageBuffer::take, HeapStorage::adopt), när bufferten inte har någon annan ägare och mottagaren har ett heapblock och ingen budget: blocket ansluter till dess fragment på samma adresser, buffertens listor för binaries och maps utanför heapen ansluter till mottagarens egna med sina debiteringar, och bufferten lämnas tom. Ingen annan kan nå blocket före eller efter, så inget ord kopieras två gånger. Övertagna fragment begär en skräpsamling först när en receive har tagit deras meddelande och deras ord når det unga blockets, så som ERTS räknar ett meddelandefragment först när det tagits emot (HeapStorage::received); nästa skräpsamling behandlar dem som ung data och kopierar ett meddelande som fortfarande står i kö som vilken rot som helst. Mindre meddelanden kopieras en gång, billigare än ett fragment vardera: direkt in i heapen hos en mottagare som inte körs, under exekutorlåset, eller direkt efter en ankomstnod för en körande mottagare. Mottagaren tar en sådan ankomst genom att kopiera dess ord som de är och flytta pekarna mellan dem med kopians avstånd, och läser objekten i adressordning så som skräpsamlaren genomsöker sina kopior (omlokaliserade ankomster); grafen gås inte igenom igen.

Skräpsamling i genererad kod

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 (wants_collection()); 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 heapen (en mindre skräpsamling om inte den senaste frigjorde mindre än hälften, binary-tryck)Orden 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 eller de överlevande plus de levande heaporden, det som är störst (aldrig mindre än 46 422), men högst de överlevande plus hälften av den budget som lämnas ledig efter heapblocketbin_vheap_sz / virtuell binary-heap
erlang:garbage_collect/0,1,2En begäran från den inbyggda funktionen (introspektion), tagen vid målets nästa safepointExplicit fullständig genomsökning

En safepoint kör en mindre skräpsamling om inte en fullständig genomsökning är aktuell: en garbage_collect-begäran, tryck från binaries utanför heapen, en heapbudget, eller ett gammalt område utan plats för de mogna orden (generationer). Den publika ProcessHeap::collect är alltid en 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 pushas; en inline-ingång tar denna väg närhelst heapen vill ha en skräpsamlingDen anropades argument x[0..arity), behållna som rötter (keep_registers)
LoophuvudCLAUSE_safepoint_v1(context) i huvudet på varje generatorloop i en comprehension, anropad endast medan heapen vill ha en skräpsamlingIngenting

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 leverans av meddelanden. 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

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. Golden-testet executables_mailbox_collection täcker hamstring, väntan med tidsgräns, djup rekursion under meddelandelast och en konsument som kvitterar 3 000 meddelanden inom --max-heap 65536.

Allokering är inte en safepoint (till skillnad från 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

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, i686-pc-windows-msvc och armv7-unknown-linux-gnueabihf vid O0 och O2 och kontrollerar att en läsning av Y:s ramord följer efter safepoint-anropet, så att optimeraren aldrig återanvänder registret som höll Y.

Skräpsamling

En Cheney-kopiering (memory/heap_collect), en fullständig genomsökning eller en mindre skräpsamling (generationer): allokera först det nya blocket (ett misslyckande ger out_of_memory och lämnar heapen orörd), kopiera sedan objektet bakom varje rotord och genomsök det nya blocket från vänster till höger medan barnen till varje kopia kopieras. En plats kontrolleras först mot föregående plats källområde (ett intervalltest), och omedelbara värden skiljs åt inline. Cons-celler genomsöks utan tolkning, och en boxad kopias platser hittas enbart från sorten i dess huvud (ett antal ospårade ord per sort, inga storlekskontroller: kopiorna är av verifierade objekt); objekt på upp till fyra ord kopieras ord för ord. 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.

Generationer

Generationsbaserad skräpsamling, så som ERTS gör den, utan dess skrivbarriär:

Kopiering mellan heapar

ProcessHeap::add(value), likvärdigt med value.copy_to(heap), returnerar en term i målheapen (BEAM:s size_object och copy_struct). Källceller läses via betrodda pekare, och en cons-cell behöver ingen tolkning:

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, samlar skräp med listan som enda rot och går igenom kopian, och skapar 1 000 kontexter som var och en håller en liten tupel. Den rapporterar bygg- och genomgångstider, använda heapord och kapacitet i heapord, sidobyte (värdallokeringar utöver heapens underliggande lagring) samt byte och heapord per kontext.