Clause
← Toda a documentação

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

Contrato do heap do processo

Os heaps dos processos seguem o desenho clássico do ERTS. Esta nota é o contrato; o plan 11 phase C (steps 8A–8I) implementou-o, e os steps posteriores indicados abaixo estendem-no. runtime.md resume a API.

O que a phase C substituiu

Antes da phase CProblemaSubstituição
Uma lista de blocos (chunks) que nunca se movemAs células não podem ser compactadas nem copiadas; a capacidade só cresceUm bloco de heap contíguo mais fragmentos, movidos por um garbage collector de cópia (8G, 8H)
Cada célula é um nó num índice std::map por processoAs palavras do heap, por si só, não são analisáveis; uma alocação no anfitrião e uma pesquisa O(log n) por célulaCélulas autodescritivas; admissão por intervalo próprio e cabeçalho (8C, 8D)
Cada célula de bitstring tem um array fixo de 64 bytes e um shared_ptr, libertado através de um registo de destrutoresCélulas grandes para dados pequenos; nada consegue mover uma célula nem encontrar as suas cópias mortasBinaries de heap de tamanho variável e células de binaries fora do heap numa lista fora do heap por processo (8B)
O Term do anfitrião fixa o heap com shared_ptr<HeapStorage>; os valores mantidos pelo runtime vivem apenas em TermsNada que um garbage collector consiga encontrar ou reescreverModelo do ERTS: o C++ só mantém palavras em bruto entre pontos seguros; os valores mantidos pelo runtime são palavras de raiz do processo; quem chama a partir do anfitrião passa raízes explícitas a collect() (8E)
Um buffer de heap por cada frame de raiz geradoNão há pilha do processo para percorrerUma pilha de frames de raiz por processo (8F)
Sem área de transbordoA alocação ou cabe no orçamento ou falhaFragmentos de heap enquanto o heap não se pode mover (8G)

O código gerado, a sua ABI e todos os resultados observáveis dos programas permanecem inalterados.

Disposição das palavras

Um termo é uma palavra do alvo (32 ou 64 bits); as codificações estão em abi.md. Cada área do heap é uma sequência de objetos que um percorredor analisa a partir da sua primeira palavra:

Nenhuma célula precisa de um alinhamento mais forte do que uma palavra. memory/heap_walk analisa uma área célula a célula e ProcessHeap::verify verifica um heap inteiro (8C). As células contêm apenas palavras e bytes, exceto o std::shared_ptr do binary fora do heap (abaixo), pelo que uma célula se move copiando as suas palavras.

TipoPalavras após o cabeçalhoPalavras rastreadas
cons (sem cabeçalho)2 palavras no totalcabeça, cauda
tuplen slots de elementostodas
map2n slots: chaves pela ordem exata dos termos, cada uma seguida do seu valortodas
native_recordendereço da RecordDefinition do runtime, depois n valores de campos pela ordem da definiçãovalores
fun_closureendereço da FunDefinition do runtime, depois n valores capturados (funs)valores
bignumpalavra de sinal, depois os limbs da magnitude, do menos significativo para o mais significativonenhuma
floating8 bytes: 1 palavra (64 bits) ou 2 palavras (32 bits)nenhuma
reference8 bytes: o número da referência (pids e referências)nenhuma
heap_binarycomprimento em bits, depois os bytes de dados arredondados para palavras (no máximo 64 bytes)nenhuma
refc_binarydeslocamento em bits, comprimento em bits, std::shared_ptr (2 palavras), ligação fora do heap: 5 palavrasnenhuma
fillern palavras não usadasnenhuma

A contagem de map é em palavras (entradas = contagem / 2). Os pids são imediatos, admitidos face aos números emitidos pelo runtime. Os tipos ainda não admitidos (identidades externas) seguirão as mesmas regras quando chegarem: as identidades e os descritores são IDs de registo em palavras não rastreadas, nunca ponteiros C++ proprietários.

Binaries fora do heap

Um binary com mais de 64 bytes é um buffer imutável que flutua fora de todos os heaps dos processos, partilhado por contagem de referências (ProcBin e Binary da BEAM).

Áreas

Dimensionamento e orçamento

Limite de memória do runtime

Admissão

Os ponteiros para o heap de um processo só são criados pelo compilador e pelo runtime dentro desse processo, e nomeiam sempre o início de um objeto; não há ponteiros interiores a detetar. A admissão (8D) é uma verificação de posse das palavras devolvidas a um processo:

  1. O endereço está alinhado à palavra dentro de uma das áreas do processo, abaixo do seu top: o bloco do heap é verificado primeiro, depois os fragmentos ordenados por endereço. As palavras alheias e obsoletas falham aqui sem qualquer carregamento.
  2. Uma palavra boxed nomeia um cabeçalho de um tipo admitido (não enchimento); uma palavra de lista nomeia uma célula cons (uma palavra que não é um cabeçalho).

Os acessores descodificam o tipo, a contagem e a carga útil a partir do próprio cabeçalho. verify() continua a ser a verificação completa de que cada slot nomeia o início de um objeto, para os testes.

Raízes e pontos seguros

ProcessContext::visit_roots enumera todas as palavras de raiz para o garbage collector (step 23). Num ponto seguro, nada mais contém palavras do heap do processo:

ProprietárioPalavras de raizNotas
Slots de termos do frameOs primeiros roots slots de cada frame na pilha (step 19)Os frames de base não têm nenhum; os índices de retoma e de bloco de tratamento são inteiros
Slots em bruto do frameNenhumaValores nativos despejados; o código gerado não guarda aí nenhuma palavra do heap num ponto seguro (regra de recarregamento do step 24)
Registosx[0..live) (ProcessStack::keep_registers)Os argumentos de uma entrada suspensa (step 43); cada push e pop limpa live
Canal de falhasCarga útil do erro (fvalue da BEAM), lista de argumentos de erlang:error/2,3, termo do stack traceReatribuídos no local; os frames do trace capturados são ponteiros de descritores para o código
Estado de trapAs palavras de termo do TrapState de um builtin que faz trap (step 43A)Libertadas quando o builtin termina ou falha
Caixa de correioTodas as mensagens na caixa de entrada de sinais e na fila de mensagens (step 45), incluindo as mensagens 'EXIT' e 'DOWN'Até um receive a retirar; reescritas no local, pelo que o cursor do receive (uma posição na lista) e o prazo do timeout permanecem válidos
Raízes explícitasO intervalo que o anfitrião passa a collect(roots) (8E)Relidas após a chamada
Lista fora do heapNenhumaAs ligações são varridas e refeitas, não rastreadas

Nenhuma célula do heap contém uma fixação. Os átomos são imediatos e a tabela de átomos nunca é recolhida. As células de fun nomeiam o código através da sua FunDefinition não rastreada, que vive tanto quanto o runtime; os módulos carregados nunca são descarregados, pelo que nem as funs nem os descritores do trace precisam de fixação. Os imediatos pequenos não são raízes.

Tal como no código C do ERTS, um Term do anfitrião é uma palavra em bruto com etiqueta, válida até ao ponto seguro seguinte do seu heap. Não fixa o armazenamento do heap; mantém um token fraco do tempo de vida do contexto e a contagem de recolhas do heap, pelo que o uso após a destruição relata expired_context e o uso após uma recolha posterior relata um erro de termo obsoleto. Um Term só é válido dentro do seu próprio processo; os outros processos só o podem ler.

O heap só se move num ponto seguro, e nunca enquanto houver uma reserva aberta:

Qualquer outro pedido devolve unsafe_point e não altera nada, nem sequer o canal de falhas de uma chamada gerada em execução. A alocação nunca move o heap: um pedido que não cabe cria um fragmento.

Recolha no código gerado

Decisão do plan 11 step 24 (2026-10-06), implementada no step 26 (implementação). O código gerado só faz a recolha em alguns safepoints onde cada termo vivo já está numa raiz. Tudo o resto, incluindo cada serviço que aloca, é uma secção crítica que nunca move o heap.

Gatilhos

Um safepoint faz a recolha quando o heap a pede; caso contrário, custa uma verificação.

GatilhoCondição no safepointEquivalente no ERTS
Heap cheioExiste algum fragmento: uma alocação não coube no bloco do heap desde a última recolhaO topo do heap atinge o fim do heap
Pressão dos binaries fora do heapAs palavras fora do heap atingem o limite do heap binário virtual: 46 422 palavras de início, após cada recolha o dobro das palavras fora do heap sobreviventes, nunca menos do que isso, mas no máximo os sobreviventes mais metade do orçamento deixado livre após o bloco do heap (step 27)bin_vheap_sz / heap binário virtual
erlang:garbage_collect/0Sempre; chega com as famílias de builtins (steps 36-37) como um safepoint forçadoVarredura completa explícita

O novo bloco é dimensionado para as palavras vivas mais as palavras da pilha em uso (o ERTS mantém a pilha dentro do bloco do heap): uma pilha profunda obtém um heap maior, pelo que uma recursão longa faz a recolha em proporção à sua alocação, em vez de voltar a percorrer toda a pilha a cada poucas centenas de palavras.

Safepoints

PontoOndeVivo fora dos slots de termos do frame
Entrada de funçãoEm CLAUSE_enter_v1 / CLAUSE_tail_v1 (e portanto na invocação pelo anfitrião), antes de o frame do chamado ser empilhadoOs argumentos do chamado x[0..arity), mantidos como raízes (keep_registers)
Início de cicloUma chamada de CLAUSE_safepoint_v1(context) no início de cada ciclo de gerador de comprehensionNada

Cada ciclo Erlang é ou uma recursão, que passa por uma entrada de função em cada passo, ou uma comprehension, que passa pelo seu início de ciclo, pelo que o lixo entre dois safepoints é limitado por código linear e por resultados individuais de serviços.

Não são safepoints (secções críticas, que continuam a alocar em fragmentos): todos os outros serviços do runtime, incluindo os serviços de alocação, construção e correspondência; CLAUSE_return_v1; a propagação de exceções; e, mais tarde, a entrega de mensagens (step 45). Os serviços podem, portanto, manter palavras do heap em bruto em C++ durante toda a sua execução, e os seus arrays de entrada e as suas saídas não precisam de recarregamento.

Processos em espera e suspensos

Plan step 51. Um processo que não está em execução nunca é recolhido: está à espera num receive, em fila após uma cedência (yield) ou um trap, ou ainda não foi iniciado, e tudo o que contém já é uma raiz (os seus frames, os registos da entrada ou da continuação em que irá retomar, o estado de trap e as suas mensagens). As mensagens que lhe são enviadas são copiadas para fragmentos do seu heap. Cada entrega acorda um processo em espera, e retomá-lo repete a entrada da sua continuação (o builtin de espera, uma continuação de trap ou a função em que cedeu), o que é um safepoint de entrada de função: a primeira coisa que um processo retomado faz é a recolha, quando o seu heap a pede. Um processo à espera num receive seletivo que salta muitas mensagens faz, por isso, a recolha à medida que elas chegam, tal como o ERTS faz a recolha de um processo quando este é escalonado a seguir. executables_mailbox_collection verifica a acumulação, a espera com timeout e a recursão profunda sob carga de mensagens, e que um consumidor que confirma 3 000 mensagens se mantém dentro de --max-heap 65536.

Rejeitado: a alocação como safepoint (test_heap da BEAM). Exigiria que cada entrada de serviço e cada termo SSA vivo através de qualquer alocação estivesse numa raiz, um recarregamento após cada serviço que aloca e um protocolo de nova tentativa em cada serviço, enquanto os dois safepoints acima já limitam o lixo.

Regra de recarregamento

Nenhum valor SSA (um valor num registo nativo) contém uma palavra do heap através de um safepoint, e nenhum ponteiro nativo atravessa nenhum (o que já é um erro em lower_frames).

Comportamento em caso de falha

Implementação

Step 26 (2026-10-06):

Step 27 (2026-10-06):

Step 27A (2026-10-06):

Protótipo

tests/prototypes/safepoint contém um ciclo ao estilo de uma comprehension na forma posterior a lower_frames (loop.ll): um termo Y calculado antes do ciclo é guardado num slot de termo e recarregado após o safepoint do início de ciclo. python tests/prototypes/safepoint/run.py compila-o para x86_64-pc-windows-msvc, aarch64-unknown-linux-gnu (palavras de 64 bits), i686-pc-windows-msvc e armv7-unknown-linux-gnueabihf (palavras de 32 bits) em O0 e O2 e verifica que um carregamento da palavra do frame de Y se segue à chamada do safepoint. Os oito passam com clang 23.1.2. Em O2 (i686), o ciclo mantém o recarregamento do slot e nunca reutiliza o registo que continha 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

Recolha

Uma cópia de Cheney com varredura completa (8H, memory/heap_collect): aloca primeiro o novo bloco (a falha é out_of_memory e deixa o heap intacto), depois marca os Terms do anfitrião como obsoletos, copia o objeto por trás de cada palavra de raiz e percorre o novo bloco da esquerda para a direita, copiando os filhos de cada cópia. O cabeçalho de um objeto boxed movido é substituído por um ponteiro boxed para a sua cópia; uma célula cons movida recebe uma cabeça a zero e uma cauda que aponta para a sua cópia. O reencaminhamento preserva a partilha. As raízes são reescritas no local, a lista fora do heap é varrida (cópias religadas pela ordem da lista, células mortas destruídas), e o bloco antigo e os fragmentos são libertados. Um heap que nunca foi alocado não é recolhido. CollectionStats relata as palavras antes, as palavras vivas, o novo bloco do heap, os fragmentos fundidos, a capacidade de slots da pilha e as palavras fora do heap.

Cópia entre heaps

ProcessHeap::add(value), ou equivalentemente value.copy_to(heap), devolve um termo do heap de destino (step 28, size_object e copy_struct da BEAM):

Medições

runtime_heap_measurements (CTest em modo completo; números impressos, não usados como gate) constrói uma lista de 100 000 elementos de tuplos {Index, Float} através de TermFactory, percorre-a de volta através de acessores verificados e cria 1 000 contextos, cada um com um tuplo pequeno. Desde o 8I também faz a recolha com a lista como única raiz e percorre a cópia. Os bytes laterais são alocações no anfitrião para além do armazenamento do heap (um índice de objetos antes do 8D, a cadeia de fragmentos desde o 8G).

RevisãoCompilaçãoConstrução / percurso no kernelPalavras do heap usadas / capacidadeBytes lateraisBytes por contextoPalavras do heap por contexto
bb09359 (lista de blocos, índice de objetos)Windows x64 Debug, clang-cl264 / 81 ms700 000 / 704 51224 002 256 (cerca de 80 por célula)66 2178 192
8D (lista de blocos, intervalo próprio)Windows x64 Debug, clang-cl185 / 147 ms700 000 / 704 5123 44066 0578 192
8G (heap de 233 palavras, cerca de 3 000 fragmentos)Windows x64 Debug, clang-cl219 / 174 ms700 000 / 706 223163 8782 377233
8I, antes da recolhaWindows x64 Debug, clang-cl216 / 173 ms700 000 / 706 223163 8782 377233
8I, após uma recolhaWindows x64 Debug, clang-clrecolha 56 ms / percurso 72 ms700 000 / 999 6310——

Face à base bb09359: os metadados laterais por célula desapareceram (de 24 MB para nada após a recolha), um contexto precisa de 2,4 KB e 233 palavras do heap em vez de 66 KB e 8 192 palavras, a construção é cerca de 20% mais rápida e o percurso é mais lento até uma recolha fundir os fragmentos (a admissão em fragmentos é uma pesquisa binária sobre cerca de 3 000 intervalos); após uma recolha, o percurso demora 72 ms. Um conjunto vivo de 700 000 palavras é recolhido em cerca de 56 ms num bloco de 999 631 palavras, o tamanho do ERTS que o mantém abaixo de 75%.