Clause
← Toda la documentación

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

Modelo de ejecución: marcos y continuaciones

Decisión del plan 11 step 17 (2026-10-05). Fija cómo las funciones Erlang generadas llaman, retornan, se suspenden y fallan una vez que llegan la recursión, las llamadas de cola (tail calls) y los procesos. El step 19 implementó las llamadas, los retornos, las llamadas de cola y los marcos (Implementación enumera lo que sigue abierto); los steps 23, 24, 26 y 43 se apoyan en él.

Decisión

Cada proceso Erlang se ejecuta sobre su propia pila plana de marcos explícitos, y el código generado pasa de una función a otra solo mediante transferencias de cola garantizadas (musttail de LLVM). Una llamada que no es de cola guarda la continuación del llamante en el marco del llamante y salta al llamado; un retorno salta de vuelta a esa continuación. Por tanto, la pila nativa se mantiene a una sola llamada de profundidad por encima del planificador, sea cual sea la profundidad de recursión en Erlang, y un proceso puede detenerse en cualquier transferencia y reanudarse más tarde en cualquier hilo.

Estado del proceso

CampoSignificado
stack, capacityUn único array de palabras ampliable; se mueve cuando crece
frame, topDesplazamientos en palabras de la cabecera del marco actual y de la primera palabra libre
x[], liveRegistros de argumentos/resultados; las primeras live palabras son raíces en una transferencia
reductionsLlamadas que quedan en la porción de tiempo
resume_atEntrada en la que continúa un proceso suspendido
canal de fallosEl canal actual de la revisión 2: razón, carga útil, traza de pila, halted

Marcos

Un marco es una cabecera fija seguida de las ranuras de la función (registros Y de BEAM). Las ranuras se ponen a cero al apilarse, como ocurre hoy con los marcos de raíces.

Palabra de cabeceraSignificado
previousDesplazamiento de la cabecera del llamante (los marcos se enlazan por desplazamientos, nunca por punteros)
functionEl descriptor de la función
resumeÍndice de continuación sobre el que conmuta el cuerpo cuando el control vuelve aquí
handlerÍndice de continuación del manejador activo más interno, 0 si no hay ninguno

El descriptor amplía el actual abi::v1::FrameDescriptor (descriptor de módulo, ranuras de átomo de módulo y de función, aridad) con los punteros a código de la entrada y del cuerpo y el número de ranuras. Las palabras de cabecera no son términos; un recorredor sigue previous y lee el número de ranuras de los descriptores. Un marco inferior propiedad del runtime está debajo de la primera llamada de cada proceso: la continuación 1 es una salida normal (resultado en x[0]) y la continuación 2 es el manejador de una excepción no capturada.

Operaciones

Una función que no hace ninguna llamada que no sea de cola y no conserva ninguna ranura a través de un punto seguro puede omitir su marco y retornar directamente al cuerpo de su llamante. Es una optimización que el compilador puede añadir más adelante, no parte del contrato.

Visibilidad de las raíces

En cada transferencia y en cada punto seguro, las raíces de un proceso son: todas las ranuras de todos los marcos de su pila, x[0..live) y la carga útil, la lista de argumentos y el término de pila del canal de fallos (que ya son raíces del proceso). Las palabras de entrega de resultados de la pila de raíces actual desaparecen; x[0] asume su papel.

Los valores nunca sobreviven a una transferencia en registros nativos ni en valores SSA. Cada cuerpo recarga la dirección de su marco desde stack + frame tras entrar, y lee los valores vivos de las ranuras. Dentro de una continuación, un servicio que puede apilar un marco o mover el heap invalida todo puntero a ranura y todo puntero al heap guardados en valores SSA; la regla de recarga para las recolecciones se fija en recolección en el código generado.

Sucesor de la pila de raíces de 8F

La pila segmentada existe solo porque el código generado mantiene punteros absolutos a marcos a través de las llamadas. Con este modelo ningún puntero a marco sobrevive a una transferencia, así que la pila pasa a ser un único bloque plano por proceso:

Destinos

musttail con la firma uniforme void (Process *) es aceptado por todos los destinos requeridos en O0 y O2 (prototipo más abajo, clang 23.1.2):

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

El backend informa un error cuando no puede respetar una llamada musttail, así que una compilación correcta es la garantía. Alternativa para un futuro destino que la rechace: un trampolín. Cada código devuelve el siguiente puntero a código al bucle del planificador en lugar de saltar (null suspende); los marcos, las raíces y los índices de continuación no cambian. Ningún destino requerido lo necesita.

Implementación

El step 19 (2026-10-05) implementa el modelo con estas decisiones y carencias:

Alternativas comparadas

Prototipo en tests/prototypes/execution_model: las mismas funciones Erlang (sum/1 con recursión de cuerpo, loop/2 con recursión de cola, fail/1 que lanza boom a profundidad N, catcher/1 que la captura) con lowering hecho a mano de tres maneras. Anfitrión: Windows x64, clang 23.1.2; los tiempos son ejecuciones únicas en O2. Ejecutar python tests/prototypes/execution_model/run.py.

Marcos explícitos + musttail (elegido)Llamadas nativas + marcos de raíces (actual)Corrutinas de LLVM (C++20)
Recursión de cuerpo de 1M de profundidadok, 17 ms; 5 palabras por marco; 17 movimientos de pila80 B de pila nativa por nivel: unos 13.000 niveles en un hilo de 1 MiBok, 60 ms; una asignación de heap de 64–80 B por llamada
10M de llamadas de colaok, 5 ms; pila constanteO0 crece 80 B por llamada; O2 solo por suerte con las sibling callssin llamadas de cola: 1M de iteraciones mantienen 1M de marcos
Cesión / reanudaciónen cada entrada; dos procesos se intercalanimposible sin una pila nativa por procesotransferencia simétrica (que a su vez es musttail)
Pila nativa a profundidad 1M136–144 Bcrece en cada nivel144–520 B
Raíces visibles para el GCranuras en marcos conocidosranuras en marcos conocidosdisposición del marco de la corrutina elegida por LLVM; los términos necesitarían una segunda copia enraizada
Excepcionesdesenrollado hasta el marco manejadorcomprobación del canal en cada retornocomprobación del canal en cada retorno

La variante con trampolín del modelo elegido supera las mismas ejecuciones (20 ms de recursión, 16 ms para 10M de llamadas de cola en O2). Descartadas:

Costes aceptados: un salto indirecto por retorno más un despacho por switch; los valores vivos a través de las llamadas se recargan desde las ranuras (ya están guardados ahí); las trazas inversas del depurador nativo solo muestran la función actual, mientras que las trazas de pila de Erlang salen de la cadena de marcos.

Evidencia del prototipo

run.py construye el modelo elegido (musttail y trampolín), la línea base nativa y el modelo de corrutinas para el anfitrión en O0 y O2, los ejecuta, y compila las funciones con lowering hecho a mano (generated.cpp, freestanding) para cada destino anterior en O0 y O2, contando las llamadas musttail en la IR y los saltos de cola en el ensamblador. Resultado del 2026-10-05: PASS. Para el modelo elegido en ambos niveles: retorno (42), recursión de cuerpo de 1.000.000 de profundidad, 10.000.000 de llamadas de cola, un error lanzado a profundidad 100.000 capturado por un marco manejador, el mismo error sin capturar (marco inferior, traza de 8 marcos fail), y dos procesos que se intercalan en porciones de 4.000 reducciones (1.002 porciones). Los 15 puntos de transferencia son musttail en O0 en todos los destinos (14 en O2 tras el inlining).

El cuerpo de sum/1 para i686-pc-windows-msvc (IR en O2, nombres abreviados). La IR de x86_64-pc-windows-msvc es la misma con palabras i64 y desplazamientos 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