Clause
← Toda la documentación

Traducido del original en inglés · 06042fa · 2026-10-09 · Leer en inglés

Contrato del heap de proceso

Los heaps de proceso siguen el diseño clásico de ERTS. Esta nota es el contrato; el plan 11 phase C (steps 8A–8I) lo implementó, y los pasos posteriores mencionados a continuación lo amplían. runtime.md resume la API.

Qué sustituyó la phase C

Antes de la phase CProblemaSustitución
Una lista de bloques que nunca se muevenLas celdas no se pueden compactar ni copiar; la capacidad solo creceUn único bloque de heap contiguo más fragmentos, movido por un recolector de copia (8G, 8H)
Cada celda es un nodo en un índice std::map por procesoLas palabras del heap por sí solas no se pueden analizar; una asignación del host y una búsqueda O(log n) por celdaCeldas autodescriptivas; admisión por rango propio y cabecera (8C, 8D)
Cada celda de bitstring tiene un array fijo de 64 bytes y un shared_ptr, liberados mediante un registro de destructoresCeldas grandes para datos pequeños; nada puede mover una celda ni encontrar sus copias muertasBinaries de heap de tamaño variable y celdas de binary fuera del heap en una lista fuera del heap por proceso (8B)
El Term del host fija el heap con shared_ptr<HeapStorage>; los valores retenidos por el runtime viven solo en TermsNada que un recolector pueda encontrar o reescribirModelo de ERTS: C++ retiene palabras en bruto solo entre puntos seguros; los valores retenidos por el runtime son palabras raíz del proceso; quienes llaman desde el host pasan raíces explícitas a collect() (8E)
Un búfer de heap por cada marco raíz generadoNo hay pila de proceso que recorrerUna pila de marcos raíz por proceso (8F)
Sin área de desbordamientoLa asignación cabe en el presupuesto o fallaFragmentos de heap mientras el heap no debe moverse (8G)

El código generado, su ABI y todo resultado observable del programa permanecen sin cambios.

Disposición de palabras

Un término es una palabra del objetivo (32 o 64 bits); las codificaciones están en abi.md. Cada área del heap es una secuencia de objetos que un recorredor analiza a partir de su primera palabra:

Ninguna celda necesita una alineación mayor que una palabra. memory/heap_walk analiza un área celda por celda y ProcessHeap::verify comprueba un heap completo (8C). Las celdas contienen solo palabras y bytes, excepto el std::shared_ptr del binary fuera del heap (más abajo), por lo que una celda se mueve copiando sus palabras.

TipoPalabras después de la cabeceraPalabras trazadas
cons (sin cabecera)2 palabras en totalcabeza, cola
tuplen slots de elementostodas
map2n slots: claves en orden exacto de términos, cada una seguida de su valortodas
native_recorddirección del RecordDefinition del runtime, y después n valores de campo en el orden de la definiciónvalores
fun_closuredirección del FunDefinition del runtime, y después n valores capturados (funs)valores
bignumpalabra de signo, y después los limbs de la magnitud, del menos significativo al más significativoninguna
floating8 bytes: 1 palabra (64 bits) o 2 palabras (32 bits)ninguna
reference8 bytes: el número de referencia (pids y referencias)ninguna
heap_binarylongitud en bits, y después los bytes de datos redondeados a palabras (como máximo 64 bytes)ninguna
refc_binarydesplazamiento en bits, longitud en bits, std::shared_ptr (2 palabras), enlace fuera del heap: 5 palabrasninguna
fillern palabras sin usarninguna

El recuento de map está en palabras (entradas = recuento / 2). Los pids son inmediatos, admitidos frente a los números emitidos por el runtime. Los tipos aún no admitidos (identidades externas) seguirán las mismas reglas cuando lleguen: las identidades y los descriptores son IDs de registro en palabras no trazadas, nunca punteros C++ propietarios.

Binaries fuera del heap

Un binary de más de 64 bytes es un búfer inmutable que flota fuera de todos los heaps de proceso, compartido mediante recuento de referencias (ProcBin y Binary de BEAM).

Áreas

Dimensionamiento y presupuesto

Límite de memoria del runtime

Admisión

Los punteros a un heap de proceso solo los crean el compilador y el runtime dentro de ese proceso, y siempre nombran el inicio de un objeto; no hay punteros interiores que detectar. La admisión (8D) es una comprobación de propiedad para las palabras que se devuelven a un proceso:

  1. La dirección está alineada a palabra dentro de una de las áreas del proceso, por debajo de su top: primero se comprueba el bloque de heap y después los fragmentos ordenados por dirección. Las palabras ajenas y obsoletas fallan aquí sin ninguna carga.
  2. Una palabra boxed nombra una cabecera de un tipo admitido (no relleno); una palabra de lista nombra una celda cons (una palabra que no es una cabecera).

Los accesores decodifican el tipo, el recuento y la carga útil a partir de la propia cabecera. verify() sigue siendo la comprobación completa de que cada slot nombra el inicio de un objeto, para las pruebas.

Raíces y puntos seguros

ProcessContext::visit_roots enumera cada palabra raíz para el recolector (step 23). En un punto seguro, nada más retiene palabras del heap del proceso:

PropietarioPalabras raízNotas
Slots de términos del marcoLos primeros roots slots de cada marco de la pila (step 19)Los marcos inferiores no tienen ninguno; los índices de reanudación y de manejador son enteros
Slots en bruto del marcoNingunaValores nativos volcados; el código generado no guarda ahí ninguna palabra del heap en un punto seguro (regla de recarga del step 24)
Registrosx[0..live) (ProcessStack::keep_registers)Los argumentos de una entrada suspendida (step 43); cada push y pop limpia live
Canal de fallosCarga útil de error (fvalue de BEAM), lista de argumentos de erlang:error/2,3, término de la traza de pilaSe revinculan en el sitio; los marcos de traza capturados son punteros a descriptores en el código
Estado de trapLas palabras de término del TrapState de un builtin que hace trap (step 43A)Se liberan cuando el builtin termina o falla
BuzónCada mensaje en la bandeja de señales y en la cola de mensajes (step 45), incluidos los mensajes 'EXIT' y 'DOWN'Hasta que un receive lo toma; se reescriben en el sitio, por lo que el cursor del receive (una posición en la lista) y el plazo del timeout siguen siendo válidos
Raíces explícitasEl intervalo que el host pasa a collect(roots) (8E)Se vuelven a leer después de la llamada
Lista fuera del heapNingunaLos enlaces se barren y se vuelven a enlazar, no se trazan

Ninguna celda del heap retiene una fijación. Los átomos son inmediatos y la tabla de átomos nunca se recolecta. Las celdas de fun nombran el código a través de su FunDefinition no trazado, que vive tanto como el runtime; los módulos cargados nunca se descargan, por lo que ni los funs ni los descriptores de traza necesitan una fijación. Los inmediatos pequeños no son raíces.

Como en el código C de ERTS, un Term del host es una palabra etiquetada en bruto válida hasta el siguiente punto seguro de su heap. No fija el almacenamiento del heap; conserva un testigo débil de la vida del contexto y el recuento de recolecciones del heap, de modo que el uso tras el desmontaje informa de expired_context y el uso tras una recolección posterior informa de un error de término obsoleto. Un Term solo es válido dentro de su propio proceso; los demás procesos solo pueden leerlo.

El heap solo se mueve en un punto seguro, y nunca mientras haya una reserva abierta:

Cualquier otra solicitud devuelve unsafe_point y no cambia nada, ni siquiera el canal de fallos de una llamada generada en ejecución. La asignación nunca mueve el heap: una solicitud que no cabe crea un fragmento.

Recolección en código generado

Decisión del plan 11 step 24 (2026-10-06), implementada en el step 26 (implementación). El código generado solo recolecta en unos pocos safepoints donde cada término vivo ya está en una raíz. Todo lo demás, incluido cada servicio que asigna, es una sección crítica que nunca mueve el heap.

Disparadores

Un safepoint recolecta cuando el heap lo solicita; en otro caso cuesta una comprobación.

DisparadorCondición en el safepointEquivalente en ERTS
Heap llenoExiste algún fragmento: una asignación no cupo en el bloque de heap desde la última recolecciónEl top del heap alcanza el final del heap
Presión de binaries fuera del heapLas palabras fuera del heap alcanzan el límite del heap binario virtual: 46,422 palabras al principio, después de cada recolección el doble de las palabras fuera del heap supervivientes, nunca menos que eso, pero como máximo los supervivientes más la mitad del presupuesto que queda libre después del bloque de heap (step 27)bin_vheap_sz / heap binario virtual
erlang:garbage_collect/0Siempre; llega con las familias de builtins (steps 36-37) como safepoint forzadoBarrido completo explícito

El nuevo bloque se dimensiona para las palabras vivas más las palabras de pila en uso (ERTS mantiene la pila dentro del bloque de heap): una pila profunda obtiene un heap mayor, de modo que una recursión larga recolecta en proporción a su asignación en lugar de volver a recorrer toda la pila cada pocos cientos de palabras.

Safepoints

PuntoDóndeVivo fuera de los slots de términos del marco
Entrada de funciónEn CLAUSE_enter_v1 / CLAUSE_tail_v1 (y por tanto en la invocación del host), antes de apilar el marco del llamadoLos argumentos del llamado x[0..arity), conservados como raíces (keep_registers)
Cabecera de bucleUna llamada a CLAUSE_safepoint_v1(context) en la cabecera de cada bucle generador de una comprehensionNada

Todo bucle de Erlang es o bien una recursión, que pasa por una entrada de función en cada paso, o bien una comprehension, que pasa por su cabecera de bucle, de modo que la basura entre dos safepoints está acotada por el código lineal y los resultados de servicios individuales.

No son safepoints (secciones críticas, que siguen asignando en fragmentos): cualquier otro servicio del runtime, incluidos los servicios de asignación, construcción y coincidencia; CLAUSE_return_v1; la propagación de excepciones; y la posterior entrega de mensajes (step 45). Por tanto, los servicios pueden retener palabras del heap en bruto en C++ durante toda su ejecución, y sus arrays de entrada y sus salidas no necesitan recarga.

Procesos en espera y suspendidos

Plan step 51. Un proceso que no se está ejecutando nunca se recolecta: está esperando en un receive, en cola tras una cesión (yield) o un trap, o aún no ha empezado, y todo lo que retiene ya es una raíz (sus marcos, los registros de la entrada o continuación en la que se reanudará, el estado de trap y sus mensajes). Los mensajes que se le envían se copian en fragmentos de su heap. Cada entrega despierta a un proceso en espera, y reanudarlo repite la entrada de su continuación (el builtin de espera, una continuación de trap o la función en la que cedió), que es un safepoint de entrada de función: lo primero que hace un proceso reanudado es recolectar cuando su heap lo solicita. Un proceso que espera en un receive selectivo que salta muchos mensajes recolecta, por tanto, a medida que llegan, igual que ERTS recolecta un proceso la próxima vez que se planifica. executables_mailbox_collection comprueba la acumulación, la espera con timeout y la recursión profunda bajo carga de mensajes, y que un consumidor que confirma 3,000 mensajes se mantiene dentro de --max-heap 65536.

Descartado: la asignación como safepoint (test_heap de BEAM). Requeriría que cada entrada de servicio y cada término SSA vivo a través de cualquier asignación estuviera en una raíz, una recarga después de cada servicio que asigna y un protocolo de reintento en cada servicio, mientras que los dos safepoints anteriores ya acotan la basura.

Regla de recarga

Ningún valor SSA (un valor en un registro nativo) retiene una palabra del heap a través de un safepoint, y ningún puntero nativo atraviesa uno en absoluto (ya es un error en lower_frames).

Comportamiento ante fallos

Implementación

Step 26 (2026-10-06):

Step 27 (2026-10-06):

Step 27A (2026-10-06):

Prototipo

tests/prototypes/safepoint contiene un bucle al estilo de una comprehension en forma posterior a lower_frames (loop.ll): un término Y calculado antes del bucle se almacena en un slot de término y se recarga después del safepoint de la cabecera del bucle. python tests/prototypes/safepoint/run.py lo compila para x86_64-pc-windows-msvc, aarch64-unknown-linux-gnu (palabras de 64 bits), i686-pc-windows-msvc y armv7-unknown-linux-gnueabihf (palabras de 32 bits) en O0 y O2 y comprueba que una carga de la palabra del marco de Y sigue a la llamada de safepoint. Las ocho pasan con clang 23.1.2. En O2 (i686) el bucle conserva la recarga del slot y nunca reutiliza el registro que contenía 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

Recolección

Una copia de Cheney de barrido completo (8H, memory/heap_collect): primero se asigna el nuevo bloque (un fallo es out_of_memory y deja el heap intacto), después se marcan como obsoletos los Terms del host, se copia el objeto detrás de cada palabra raíz y se recorre el nuevo bloque de izquierda a derecha, copiando los hijos de cada copia. La cabecera de un objeto boxed movido se sustituye por un puntero boxed a su copia; una celda cons movida recibe una cabeza a cero y una cola que apunta a su copia. El reenvío preserva la compartición. Las raíces se reescriben en el sitio, se barre la lista fuera del heap (las copias se vuelven a enlazar en el orden de la lista y las celdas muertas se destruyen), y se liberan el bloque antiguo y los fragmentos. Un heap que nunca se asignó no se recolecta. CollectionStats informa de las palabras anteriores, las palabras vivas, el nuevo bloque de heap, los fragmentos fusionados, la capacidad de slots de la pila y las palabras fuera del heap.

Copia entre heaps

ProcessHeap::add(value), o equivalentemente value.copy_to(heap), devuelve un término del heap de destino (step 28, size_object y copy_struct de BEAM):

Mediciones

runtime_heap_measurements (CTest en modo completo; los números se imprimen, no actúan como puerta) construye una lista de 100,000 elementos de tuplas {Index, Float} mediante TermFactory, la recorre de vuelta mediante accesores comprobados y crea 1,000 contextos que retienen cada uno una tupla pequeña. Desde 8I también recolecta con la lista como única raíz y recorre la copia. Los bytes laterales son asignaciones del host más allá del respaldo del heap (un índice de objetos antes de 8D, la cadena de fragmentos desde 8G).

RevisiónCompilaciónConstrucción del kernel / recorridoPalabras de heap usadas / capacidadBytes lateralesBytes por contextoPalabras de heap por contexto
bb09359 (lista de bloques, índice de objetos)Windows x64 Debug, clang-cl264 / 81 ms700,000 / 704,51224,002,256 (unos 80 por celda)66,2178,192
8D (lista de bloques, rango propio)Windows x64 Debug, clang-cl185 / 147 ms700,000 / 704,5123,44066,0578,192
8G (heap de 233 palabras, unos 3,000 fragmentos)Windows x64 Debug, clang-cl219 / 174 ms700,000 / 706,223163,8782,377233
8I, antes de la recolecciónWindows x64 Debug, clang-cl216 / 173 ms700,000 / 706,223163,8782,377233
8I, después de una recolecciónWindows x64 Debug, clang-clrecolección 56 ms / recorrido 72 ms700,000 / 999,6310——

Frente a la línea base bb09359: los metadatos laterales por celda han desaparecido (de 24 MB a ninguno una vez recolectado), un contexto necesita 2.4 KB y 233 palabras de heap en lugar de 66 KB y 8,192 palabras, la construcción es aproximadamente un 20% más rápida, y el recorrido es más lento hasta que una recolección fusiona los fragmentos (la admisión de fragmentos es una búsqueda binaria sobre unos 3,000 rangos); después de una, el recorrido tarda 72 ms. Un conjunto vivo de 700,000 palabras se recolecta en unos 56 ms en un bloque de 999,631 palabras, el tamaño de ERTS que lo mantiene por debajo del 75%.