Clause
← Toda a documentação

Traduzido do original em inglês · 06042fa · 2026-10-09 · Ler em inglês

Modelo de execução: frames e continuações

Decisão do plano 11, step 17 (2026-10-05). Define a forma como as funções Erlang geradas chamam, retornam, se suspendem e falham, a partir do momento em que chegam a recursão, as chamadas de cauda e os processos. O step 19 implementou as chamadas, os retornos, as chamadas de cauda e os frames (Implementação lista o que ainda está em aberto); os steps 23, 24, 26 e 43 baseiam-se nele.

Decisão

Cada processo Erlang corre na sua própria pilha plana de frames explícitos, e o código gerado só passa de uma função para outra através de transferências de cauda garantidas (musttail do LLVM). Uma chamada que não é de cauda guarda a continuação do chamador no frame do chamador e salta para a função chamada; um retorno salta de volta para essa continuação. A pilha nativa fica, portanto, com uma única chamada de profundidade acima do escalonador, seja qual for a profundidade da recursão Erlang, e um processo pode parar em qualquer transferência e retomar mais tarde em qualquer thread.

Estado do processo

CampoSignificado
stack, capacityUm único array de palavras que pode crescer; desloca-se quando cresce
frame, topDeslocamentos, em palavras, do cabeçalho do frame atual e da primeira palavra livre
x[], liveRegistos de argumentos/resultados; as primeiras live palavras são raízes numa transferência
reductionsChamadas que restam na fatia de tempo
resume_atEntrada na qual um processo suspenso continua
canal de falhasO canal atual da revisão 2: razão, payload, stack trace, halted

Frames

Um frame é um cabeçalho fixo seguido dos slots da função (registos Y do BEAM). Os slots são postos a zero no push, tal como os frames de raízes atuais.

Palavra do cabeçalhoSignificado
previousDeslocamento do cabeçalho do chamador (os frames ligam-se por deslocamentos, nunca por apontadores)
functionO descritor da função
resumeÍndice de continuação sobre o qual o corpo faz o switch quando o controlo regressa aqui
handlerÍndice de continuação do handler ativo mais interior, 0 se não houver nenhum

O descritor estende o atual abi::v1::FrameDescriptor (descritor do módulo, slots dos átomos do módulo e da função, aridade) com os apontadores de código da entrada e do corpo e com o número de slots. As palavras do cabeçalho não são termos; quem percorre a pilha segue previous e lê o número de slots nos descritores. Um frame de fundo pertencente ao runtime fica por baixo da primeira chamada de cada processo: a continuação 1 é uma saída normal (resultado em x[0]), a continuação 2 é o handler de uma exceção não capturada.

Operações

Uma função que não faça nenhuma chamada que não seja de cauda e não mantenha nenhum slot através de um safepoint pode dispensar o seu frame e retornar diretamente para o corpo do chamador. Trata-se de uma otimização que o compilador pode acrescentar mais tarde, não de parte do contrato.

Visibilidade das raízes

Em cada transferência e em cada safepoint, as raízes de um processo são: todos os slots de todos os frames da sua pilha, x[0..live), e o payload, a lista de argumentos e o termo de pilha do canal de falhas (já raízes do processo). As palavras de passagem do resultado da pilha de raízes atual desaparecem; x[0] assume o seu papel.

Os valores nunca sobrevivem a uma transferência em registos nativos ou em valores SSA. Cada corpo recarrega o endereço do seu frame a partir de stack + frame depois de entrar, e lê os valores vivos dos slots. Dentro de uma continuação, um serviço que possa fazer push de um frame ou deslocar o heap invalida todos os apontadores de slots e de heap mantidos em valores SSA; a regra de recarregamento para as recolhas está fixada em recolha no código gerado.

Sucessora da pilha de raízes do 8F

A pilha segmentada só existe porque o código gerado mantém apontadores de frame absolutos através das chamadas. Neste modelo, nenhum apontador de frame sobrevive a uma transferência, pelo que a pilha passa a ser um único bloco plano por processo:

Alvos

musttail com a assinatura uniforme void (Process *) é aceite por todos os alvos exigidos em O0 e O2 (protótipo abaixo, clang 23.1.2):

AlvoPalavraResultado
x86_64-pc-windows-msvc, x86_64-unknown-linux-gnu64saltos de cauda
i686-pc-windows-msvc, i686-unknown-linux-gnu32saltos de cauda
aarch64-unknown-linux-gnu, arm64-apple-macosx14.064saltos de cauda
armv7-unknown-linux-gnueabihf32saltos de cauda

O backend indica um erro quando não consegue honrar uma chamada musttail, pelo que uma compilação bem-sucedida é a garantia. Alternativa para um alvo futuro que a rejeite: um trampolim. Cada código devolve o apontador de código seguinte ao ciclo do escalonador em vez de saltar (nulo suspende); os frames, as raízes e os índices de continuação ficam inalterados. Nenhum alvo exigido precisa dele.

Implementação

O step 19 (2026-10-05) implementa o modelo com estas escolhas e lacunas:

Alternativas comparadas

Protótipo em tests/prototypes/execution_model: as mesmas funções Erlang (sum/1 com recursão no corpo, loop/2 com recursão de cauda, fail/1 a lançar boom à profundidade N, catcher/1 a capturá-lo) convertidas à mão de três formas. Anfitrião: Windows x64, clang 23.1.2; os tempos são execuções únicas em O2. Executar python tests/prototypes/execution_model/run.py.

Frames explícitos + musttail (escolhido)Chamadas nativas + frames de raízes (atual)Corrotinas LLVM (C++20)
Recursão no corpo com 1M de profundidadeok, 17 ms; 5 palavras por frame; 17 deslocações da pilha80 B de pilha nativa por nível: cerca de 13 000 níveis numa thread de 1 MiBok, 60 ms; uma alocação no heap de 64–80 B por chamada
10M chamadas de caudaok, 5 ms; pilha constanteem O0 cresce 80 B por chamada; em O2 só por sorte com sibling callssem chamadas de cauda: 1M iterações mantêm 1M frames
Yield / retomaem cada entrada; dois processos intercalam-seimpossível sem uma pilha nativa por processotransferência simétrica (ela própria musttail)
Pilha nativa à profundidade 1M136–144 Bcresce por nível144–520 B
Raízes visíveis para o GCslots em frames conhecidosslots em frames conhecidosdisposição do frame da corrotina escolhida pelo LLVM; os termos precisariam de uma segunda cópia enraizada
Exceçõesdesenrolar até ao frame do handlerverificação do canal em cada retornoverificação do canal em cada retorno

A variante com trampolim do modelo escolhido passa as mesmas execuções (20 ms de recursão, 16 ms para 10M chamadas de cauda em O2). Rejeitadas:

Custos aceites: um salto indireto por retorno mais um despacho por switch; os valores vivos através das chamadas são recarregados dos slots (já lá estão guardados); os backtraces dos depuradores nativos mostram apenas a função atual, enquanto os stack traces Erlang provêm da cadeia de frames.

Evidência do protótipo

run.py compila o modelo escolhido (musttail e trampolim), a base nativa e o modelo de corrotinas para o anfitrião em O0 e O2, executa-os e compila as funções convertidas à mão (generated.cpp, freestanding) para cada alvo acima em O0 e O2, contando as chamadas musttail no IR e os saltos de cauda no assembly. Resultado de 2026-10-05: PASS. Para o modelo escolhido, em ambos os níveis: retorno (42), recursão no corpo com 1 000 000 de profundidade, 10 000 000 chamadas de cauda, um erro lançado à profundidade 100 000 capturado por um frame de handler, o mesmo erro não capturado (frame de fundo, trace de 8 frames fail), e dois processos intercalados em fatias de 4000 reduções (1002 fatias). Os 15 pontos de transferência são todos musttail em O0 em todos os alvos (14 em O2 após inlining).

O corpo de sum/1 para i686-pc-windows-msvc (IR em O2, nomes encurtados). O IR de x86_64-pc-windows-msvc é o mesmo, com palavras i64 e deslocamentos duplicados.

%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