Clause
← Toute la documentation

Traduit de l'original anglais · 06042fa · 2026-10-09 · Lire en anglais

Contrat du tas de processus

Les tas de processus suivent la conception classique d'ERTS. Cette note en est le contrat ; le plan 11 phase C (steps 8A–8I) l'a livré, et les steps ultérieurs nommés ci-dessous l'étendent. runtime.md résume l'API.

Ce que la phase C a remplacé

Avant la phase CProblèmeRemplacement
Une liste de blocs (chunks) qui ne bougent jamaisLes cellules ne peuvent être ni compactées ni copiées ; la capacité ne fait que croîtreUn bloc de tas contigu plus des fragments, déplacés par un ramasse-miettes par copie (8G, 8H)
Chaque cellule est un nœud dans un index std::map par processusLes mots du tas seuls ne sont pas analysables ; une allocation hôte et une recherche en O(log n) par celluleCellules autodescriptives ; admission par plage possédée et en-tête (8C, 8D)
Chaque cellule de bitstring a un tableau fixe de 64 octets et un shared_ptr, libérés via un registre de destructeursGrandes cellules pour de petites données ; rien ne peut déplacer une cellule ni retrouver ses copies mortesBinaries de tas de taille variable et cellules de binaries hors tas dans une liste hors tas par processus (8B)
Le Term hôte épingle le tas avec shared_ptr<HeapStorage> ; les valeurs détenues par le runtime vivent uniquement dans des TermRien qu'un ramasse-miettes puisse trouver ou réécrireModèle ERTS : le C++ ne détient des mots bruts qu'entre deux points sûrs ; les valeurs détenues par le runtime sont des mots racines du processus ; les appelants hôtes passent des racines explicites à collect() (8E)
Un tampon de tas par cadre racine généréAucune pile de processus à parcourirUne pile de cadres racines par processus (8F)
Pas de zone de débordementUne allocation tient dans le budget ou échoueDes fragments de tas tant que le tas ne doit pas bouger (8G)

Le code généré, son ABI et tout résultat observable des programmes restent inchangés.

Disposition des mots

Un terme est un mot de la cible (32 ou 64 bits) ; les encodages sont décrits dans abi.md. Chaque zone du tas est une suite d'objets qu'un parcoureur analyse à partir de son premier mot :

Aucune cellule n'a besoin d'un alignement plus strict qu'un mot. memory/heap_walk analyse une zone cellule par cellule et ProcessHeap::verify vérifie un tas entier (8C). Les cellules ne contiennent que des mots et des octets, à l'exception du std::shared_ptr du binary hors tas (ci-dessous), si bien qu'une cellule se déplace en copiant ses mots.

TypeMots après l'en-têteMots tracés
cons (sans en-tête)2 mots au totaltête, queue
tuplen emplacements d'élémentstous
map2n emplacements : les clés dans l'ordre exact des termes, chacune suivie de sa valeurtous
native_recordadresse du RecordDefinition du runtime, puis n valeurs de champs dans l'ordre de définitionvaleurs
fun_closureadresse du FunDefinition du runtime, puis n valeurs capturées (funs)valeurs
bignummot de signe, puis les limbes de la magnitude, les moins significatifs d'abordaucun
floating8 octets : 1 mot (64 bits) ou 2 mots (32 bits)aucun
reference8 octets : le numéro de référence (pids et références)aucun
heap_binarylongueur en bits, puis les octets de données arrondis aux mots (au plus 64 octets)aucun
refc_binarydécalage en bits, longueur en bits, std::shared_ptr (2 mots), lien hors tas : 5 motsaucun
fillern mots inutilisésaucun

Le compte d'une map est en mots (entrées = compte / 2). Les pids sont des immédiats, admis d'après les numéros émis par le runtime. Les types pas encore admis (identités externes) suivront les mêmes règles à leur arrivée : les identités et les descripteurs sont des identifiants de registre dans des mots non tracés, jamais des pointeurs C++ propriétaires.

Binaries hors tas

Un binary de plus de 64 octets est un tampon immuable qui flotte en dehors de tout tas de processus, partagé par comptage de références (ProcBin et Binary de BEAM).

Zones

Dimensionnement et budget

Limite mémoire du runtime

Admission

Les pointeurs vers un tas de processus ne sont créés que par le compilateur et le runtime à l'intérieur de ce processus, et désignent toujours un début d'objet ; il n'y a pas de pointeurs intérieurs à détecter. L'admission (8D) est une vérification de propriété pour les mots rendus à un processus :

  1. L'adresse est alignée sur un mot à l'intérieur de l'une des zones du processus, sous son top : le bloc de tas est vérifié en premier, puis les fragments triés par adresse. Les mots étrangers ou périmés échouent ici sans aucun chargement.
  2. Un mot boxed désigne un en-tête d'un type admis (pas un remplissage) ; un mot de liste désigne une cellule cons (un mot qui n'est pas un en-tête).

Les accesseurs décodent le type, le compte et la charge utile à partir de l'en-tête lui-même. verify() reste la vérification complète, pour les tests, que chaque emplacement désigne un début d'objet.

Racines et points sûrs

ProcessContext::visit_roots énumère chaque mot racine pour le ramasse-miettes (step 23). À un point sûr, rien d'autre ne détient de mots du tas du processus :

PropriétaireMots racinesNotes
Emplacements de termes des cadresLes roots premiers emplacements de chaque cadre de la pile (step 19)Les cadres du fond n'en ont aucun ; les indices de reprise et de gestionnaire sont des entiers
Emplacements bruts des cadresAucunValeurs natives déversées ; le code généré n'y garde aucun mot du tas à un point sûr (règle de rechargement du step 24)
Registresx[0..live) (ProcessStack::keep_registers)Les arguments d'une entrée suspendue (step 43) ; chaque empilement et dépilement efface live
Canal d'échecCharge utile d'erreur (fvalue de BEAM), liste d'arguments de erlang:error/2,3, terme de trace de pileReliés sur place ; les cadres de trace capturés sont des pointeurs de descripteurs vers le code
État d'interception (trap)Les mots de terme du TrapState d'un builtin qui intercepte (step 43A)Libérés lorsque le builtin se termine ou échoue
Boîte aux lettresChaque message dans la boîte de réception des signaux et la file de messages (step 45), y compris les messages 'EXIT' et 'DOWN'Jusqu'à ce qu'un receive le prenne ; réécrits sur place, donc le curseur de receive (une position dans la liste) et l'échéance du timeout restent valides
Racines explicitesLa plage qu'un hôte passe à collect(roots) (8E)Relues après l'appel
Liste hors tasAucunLes liens sont balayés et rechaînés, non tracés

Aucune cellule du tas ne détient d'épingle. Les atomes sont des immédiats et la table des atomes n'est jamais collectée. Les cellules de fun désignent le code via leur FunDefinition non tracé, qui vit aussi longtemps que le runtime ; les modules chargés ne sont jamais déchargés, donc ni les funs ni les descripteurs de trace n'ont besoin d'épingle. Les petits immédiats ne sont pas des racines.

Comme dans le code C d'ERTS, un Term hôte est un mot étiqueté brut valide jusqu'au prochain point sûr de son tas. Il n'épingle pas le stockage du tas ; il garde un jeton faible de durée de vie du contexte et le compteur de collectes du tas, si bien qu'une utilisation après le démontage signale expired_context et qu'une utilisation après une collecte ultérieure signale une erreur de terme périmé. Un Term n'est valide qu'à l'intérieur de son propre processus ; les autres processus peuvent seulement le lire.

Le tas ne se déplace qu'à un point sûr, et jamais tant qu'une réservation est ouverte :

Toute autre requête retourne unsafe_point et ne change rien, pas même le canal d'échec d'un appel généré en cours. L'allocation ne déplace jamais le tas : une requête qui ne tient pas crée un fragment.

Collecte dans le code généré

Décision du plan 11 step 24 (2026-10-06), implémentée au step 26 (implémentation). Le code généré n'effectue de collecte qu'à quelques points sûrs (safepoints) où chaque terme vivant se trouve déjà dans une racine. Tout le reste, y compris chaque service qui alloue, est une section critique qui ne déplace jamais le tas.

Déclencheurs

Un point sûr effectue une collecte lorsque le tas le demande ; sinon il coûte une vérification.

DéclencheurCondition au point sûrÉquivalent ERTS
Tas pleinUn fragment existe : une allocation n'a pas tenu dans le bloc de tas depuis la dernière collecteLe sommet du tas atteint la fin du tas
Pression des binaries hors tasLes mots hors tas atteignent la limite du tas binaire virtuel : 46 422 mots au départ, après chaque collecte le double des mots hors tas survivants, jamais moins, mais au plus les survivants plus la moitié du budget laissé libre après le bloc de tas (step 27)bin_vheap_sz / tas binaire virtuel
erlang:garbage_collect/0Toujours ; arrive avec les familles de builtins (steps 36-37) sous forme de point sûr forcéBalayage complet explicite

Le nouveau bloc est dimensionné pour les mots vivants plus les mots de pile utilisés (ERTS garde la pile à l'intérieur du bloc de tas) : une pile profonde obtient un tas plus grand, si bien qu'une longue récursion effectue des collectes proportionnelles à ses allocations plutôt que de reparcourir toute la pile toutes les quelques centaines de mots.

Points sûrs

PointEmplacementVivant hors des emplacements de termes des cadres
Entrée de fonctionDans CLAUSE_enter_v1 / CLAUSE_tail_v1 (et donc l'invocation hôte), avant l'empilement du cadre de l'appeléLes arguments de l'appelé x[0..arity), gardés comme racines (keep_registers)
Tête de boucleUn appel de CLAUSE_safepoint_v1(context) en tête de chaque boucle de générateur de comprehensionRien

Toute boucle Erlang est soit une récursion, qui passe par une entrée de fonction à chaque itération, soit une comprehension, qui passe par sa tête de boucle, donc les déchets entre deux points sûrs sont bornés par du code linéaire et des résultats de service uniques.

Ne sont pas des points sûrs (sections critiques, qui continuent d'allouer dans des fragments) : tous les autres services du runtime, y compris les services d'allocation, de construction et de correspondance ; CLAUSE_return_v1 ; la propagation des exceptions ; et plus tard la remise des messages (step 45). Les services peuvent donc détenir des mots bruts du tas en C++ pendant toute leur exécution, et leurs tableaux d'entrée et leurs sorties n'ont pas besoin d'être rechargés.

Processus en attente et suspendus

Plan step 51. Un processus qui ne s'exécute pas n'est jamais collecté : il attend dans un receive, est en file après un yield ou un trap, ou n'a pas encore démarré, et tout ce qu'il détient est déjà une racine (ses cadres, les registres de l'entrée ou de la continuation à laquelle il reprendra, l'état de trap et ses messages). Les messages qui lui sont envoyés sont copiés dans des fragments de son tas. Chaque remise réveille un processus en attente, et sa reprise répète l'entrée de sa continuation (le builtin d'attente, une continuation de trap ou la fonction à laquelle il a cédé), ce qui est un point sûr d'entrée de fonction : la première chose que fait un processus repris est une collecte si son tas la demande. Un processus qui attend dans un receive sélectif sautant de nombreux messages effectue donc des collectes à mesure qu'ils arrivent, de même qu'ERTS collecte un processus lors de sa prochaine planification. executables_mailbox_collection vérifie l'accumulation, l'attente avec timeout et la récursion profonde sous charge de messages, ainsi qu'un consommateur accusant réception de 3 000 messages reste dans --max-heap 65536.

Rejeté : l'allocation comme point sûr (test_heap de BEAM). Cela exigerait que chaque entrée de service et chaque terme SSA vivant au travers d'une allocation soit dans une racine, un rechargement après chaque service qui alloue et un protocole de nouvelle tentative dans chaque service, alors que les deux points sûrs ci-dessus bornent déjà les déchets.

Règle de rechargement

Aucune valeur SSA (une valeur dans un registre natif) ne détient de mot du tas à travers un point sûr, et aucun pointeur natif n'en traverse un du tout (c'est déjà une erreur dans lower_frames).

Comportement en cas d'échec

Implémentation

Step 26 (2026-10-06) :

Step 27 (2026-10-06) :

Step 27A (2026-10-06) :

Prototype

tests/prototypes/safepoint contient une boucle de style comprehension sous forme post-lower_frames (loop.ll) : un terme Y calculé avant la boucle est stocké dans un emplacement de terme et rechargé après le point sûr de tête de boucle. python tests/prototypes/safepoint/run.py le compile pour x86_64-pc-windows-msvc, aarch64-unknown-linux-gnu (mots de 64 bits), i686-pc-windows-msvc et armv7-unknown-linux-gnueabihf (mots de 32 bits) en O0 et O2 et vérifie qu'un chargement du mot de cadre de Y suit l'appel de point sûr. Les huit réussissent avec clang 23.1.2. En O2 (i686), la boucle conserve le rechargement depuis l'emplacement et ne réutilise jamais le registre qui contenait 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

Collecte

Une copie de Cheney à balayage complet (8H, memory/heap_collect) : allouer d'abord le nouveau bloc (un échec donne out_of_memory et laisse le tas intact), puis marquer comme périmés les Term hôtes, copier l'objet derrière chaque mot racine et parcourir le nouveau bloc de gauche à droite en copiant les enfants de chaque copie. L'en-tête d'un objet boxed déplacé est remplacé par un pointeur boxed vers sa copie ; une cellule cons déplacée reçoit une tête nulle et une queue pointant vers sa copie. Le renvoi préserve le partage. Les racines sont réécrites sur place, la liste hors tas est balayée (copies rechaînées dans l'ordre de la liste, cellules mortes détruites), et l'ancien bloc et les fragments sont libérés. Un tas jamais alloué n'est pas collecté. CollectionStats indique les mots avant, les mots vivants, le nouveau bloc de tas, les fragments fusionnés, la capacité en emplacements de la pile et les mots hors tas.

Copie entre tas

ProcessHeap::add(value), ou de manière équivalente value.copy_to(heap), retourne un terme du tas de destination (step 28, size_object et copy_struct de BEAM) :

Mesures

runtime_heap_measurements (CTest en mode complet ; chiffres affichés, sans seuil) construit une liste de 100 000 éléments de tuples {Index, Float} via TermFactory, la parcourt de nouveau via des accesseurs vérifiés, et crée 1 000 contextes qui détiennent chacun un petit tuple. Depuis 8I, il effectue aussi une collecte avec la liste comme seule racine et parcourt la copie. Les octets annexes sont les allocations hôtes au-delà du stockage du tas (un index d'objets avant 8D, la chaîne de fragments depuis 8G).

RévisionBuildConstruction / parcours du noyauMots de tas utilisés / capacitéOctets annexesOctets par contexteMots de tas par contexte
bb09359 (liste de blocs, index d'objets)Windows x64 Debug, clang-cl264 / 81 ms700 000 / 704 51224 002 256 (environ 80 par cellule)66 2178 192
8D (liste de blocs, plage possédée)Windows x64 Debug, clang-cl185 / 147 ms700 000 / 704 5123 44066 0578 192
8G (tas de 233 mots, environ 3 000 fragments)Windows x64 Debug, clang-cl219 / 174 ms700 000 / 706 223163 8782 377233
8I, avant collecteWindows x64 Debug, clang-cl216 / 173 ms700 000 / 706 223163 8782 377233
8I, après une collecteWindows x64 Debug, clang-clcollecte 56 ms / parcours 72 ms700 000 / 999 6310——

Par rapport à la référence bb09359 : les métadonnées annexes par cellule ont disparu (de 24 Mo à rien une fois la collecte faite), un contexte a besoin de 2,4 Ko et de 233 mots de tas au lieu de 66 Ko et 8 192 mots, la construction est environ 20 % plus rapide, et le parcours est plus lent jusqu'à ce qu'une collecte fusionne les fragments (l'admission par fragment est une recherche dichotomique sur environ 3 000 plages) ; après une collecte, le parcours prend 72 ms. Un ensemble vivant de 700 000 mots est collecté en environ 56 ms dans un bloc de 999 631 mots, la taille ERTS qui le maintient sous 75 %.