Clause
← Toute la documentation

L'original anglais a changé depuis cette traduction ; certaines parties peuvent donc être obsolètes. Lire en anglais

Traduit de l'original anglais · 1e3a39c8-dirty · 2026-10-26

Modèle d'exécution : cadres et continuations

Comment les fonctions Erlang générées appellent, retournent, se suspendent et échouent. Le modèle a été choisi après la comparaison de prototypes décrite dans alternatives comparées ; le côté code généré de son ABI se trouve dans cadres et transferts.

Décision

Chaque processus Erlang s'exécute sur sa propre pile plate de cadres explicites (frames), et le code généré ne passe d'une fonction à l'autre que par des transferts terminaux garantis (LLVM musttail). Un appel non terminal stocke la continuation de l'appelant dans le cadre de l'appelant et saute vers l'appelé ; un retour saute de nouveau vers cette continuation. La pile native reste donc à un seul niveau d'appel au-dessus de l'ordonnanceur, quelle que soit la profondeur de récursion Erlang, et un processus peut s'arrêter à n'importe quel transfert et reprendre plus tard sur n'importe quel thread.

État du processus

abi::v1::FrameState se trouve au début de chaque contexte, de sorte que le code généré le lit et le met à jour inline :

ChampSignification
stack, top, limitLe bloc de pile (se déplace lorsqu'il grandit), son premier mot libre, et les mots qu'un empilement peut atteindre sans grandir ni dépasser la limite de pile
frameDécalage en mots de l'en-tête du cadre courant
registers, liveLes 256 registres d'arguments/de résultat ; les live premiers mots sont des racines lors d'un transfert
reductionsEntrées de fonction restantes dans la tranche de temps
slow, failedBits qui envoient les transferts vers les services (slow_collect, slow_profile) ; non nul tant que le canal d'échec contient un échec

heap_top/heap_limit, runtime et self servent à l'allocation inline, aux chargements d'atomes et à self/0 (cadres et transferts). Un processus suspendu enregistre l'entrée à laquelle il reprend dans ProcessStack::resume_.

Cadres

Un cadre est un en-tête de 4 mots suivi des emplacements de la fonction (registres Y de BEAM) : les emplacements de termes, qui sont des racines, puis les emplacements bruts pour les valeurs natives déversées.

Mot d'en-têteSignification
previousDécalage de l'en-tête de l'appelant (les cadres sont chaînés par décalages, jamais par pointeurs)
functionLe FrameDescriptor de la fonction
resumeIndice de continuation sur lequel le corps aiguille lorsque le contrôle revient ici
handlerIndice de gestionnaire ; toujours 0, car les échecs retournent à travers les appelants

Le descripteur <symbol>.frame nomme le module, la fonction et l'arité et contient le pointeur de code du corps et le nombre d'emplacements (abi). Les mots d'en-tête ne sont pas des termes ; un parcours suit previous et lit le nombre d'emplacements dans les descripteurs. Un cadre de fond (bottom frame) appartenant au runtime se trouve sous le premier appel de chaque processus et de chaque invocation par l'hôte : y retourner stocke le résultat dans le premier registre et termine l'appel natif.

Opérations

Chaque appel empile un cadre ; les fonctions feuilles ne s'en dispensent pas, de sorte que chaque fonction conserve son entrée dans la trace de pile.

Visibilité des racines

À chaque transfert et à chaque point sûr, les racines d'un processus sont : les emplacements de termes de tous les cadres de sa pile, x[0..live), et la charge utile, la liste d'arguments et le terme de pile du canal d'échec (racines).

Les valeurs ne survivent jamais à un transfert dans des registres natifs ou des valeurs SSA, sauf les paramètres du transfert lui-même. Chaque corps recharge l'adresse de son cadre depuis stack + frame après y être entré, et lit les valeurs vivantes dans les emplacements. Au sein d'une continuation, un service qui peut empiler un cadre ou déplacer le tas invalide tout pointeur d'emplacement et tout pointeur de tas conservés dans des valeurs SSA ; la règle de rechargement pour les collectes est fixée dans règle de rechargement.

Le bloc de pile grandit en doublant et en se déplaçant tant que la limite à l'échelle du runtime le permet, ne rétrécit jamais pendant l'exécution et est séparé du bloc de tas. Il n'a pas de borne sur le nombre de cadres ; les mots de pile comptent dans la limite mémoire du runtime.

Cibles

musttail avec une signature de code uniforme est accepté par chaque cible requise en O0 et O2 (prototype ci-dessous, clang 23.1.2) :

CibleMotRésultat
x86_64-pc-windows-msvc, x86_64-unknown-linux-gnu64sauts terminaux
i686-pc-windows-msvc, i686-unknown-linux-gnu32sauts terminaux
aarch64-unknown-linux-gnu, arm64-apple-macosx14.064sauts terminaux
armv7-unknown-linux-gnueabihf32sauts terminaux

Le backend signale une erreur lorsqu'il ne peut pas honorer un appel musttail, donc une compilation réussie constitue la garantie. Solution de repli pour une future cible qui le rejetterait : un trampoline, où chaque code retourne le pointeur de code suivant à la boucle de l'ordonnanceur au lieu de sauter ; les cadres, les racines et les indices de continuation sont inchangés. Aucune cible requise n'en a besoin.

Implémentation

Alternatives comparées

tests/prototypes/execution_model abaisse à la main les mêmes fonctions Erlang (récursion de corps sum/1, récursion terminale loop/2, fail/1 levant une erreur à la profondeur N, catcher/1 la capturant) de trois façons et les compile pour chaque cible ci-dessus en O0 et O2 (python tests/prototypes/execution_model/run.py). Sous Windows x64 en O2, le modèle choisi a exécuté une récursion de corps à 1M de profondeur en 17 ms avec 5 mots par cadre et 10M d'appels terminaux en pile constante ; sa variante trampoline a réussi les mêmes exécutions. Rejetées :

Coûts acceptés : un saut indirect par retour plus un aiguillage par switch ; les valeurs vivantes à travers les appels sont rechargées depuis les emplacements ; les backtraces du débogueur natif ne montrent que la fonction courante, tandis que les traces de pile Erlang proviennent de la chaîne de cadres (débogage).