Clause
← Toda la documentación

El original en inglés ha cambiado desde que se hizo esta traducción, por lo que partes de ella pueden estar desactualizadas. Leer en inglés

Traducido del original en inglés · 1e3a39c8-dirty · 2026-10-26

Modelo de ejecución: marcos y continuaciones

Cómo las funciones Erlang generadas llaman, retornan, se suspenden y fallan. El modelo se eligió tras la comparación de prototipos de alternativas comparadas; la parte de su ABI correspondiente al código generado está en marcos y transferencias.

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

abi::v1::FrameState encabeza cada contexto, de modo que el código generado lo lee y lo actualiza en línea:

CampoSignificado
stack, top, limitEl bloque de pila (se mueve cuando crece), su primera palabra libre y las palabras que un apilamiento puede alcanzar sin crecer ni superar el límite de la pila
frameDesplazamiento en palabras de la cabecera del marco actual
registers, liveLos 256 registros de argumentos/resultados; las primeras live palabras son raíces en una transferencia
reductionsEntradas de función que quedan en la porción de tiempo
slow, failedBits que envían las transferencias a los servicios (slow_collect, slow_profile); distinto de cero mientras el canal de fallos contiene un fallo

heap_top/heap_limit, runtime y self sirven a la asignación en línea, a la carga de átomos y a self/0 (marcos y transferencias). Un proceso suspendido registra en ProcessStack::resume_ la entrada en la que continúa.

Marcos

Un marco es una cabecera de 4 palabras seguida de las ranuras de la función (registros Y de BEAM): ranuras de términos, que son raíces, y después ranuras en bruto para valores nativos volcados.

Palabra de cabeceraSignificado
previousDesplazamiento de la cabecera del llamante (los marcos se enlazan por desplazamientos, nunca por punteros)
functionEl FrameDescriptor de la función
resumeÍndice de continuación sobre el que conmuta el cuerpo cuando el control vuelve aquí
handlerÍndice del manejador; siempre 0, ya que los fallos retornan a través de los llamantes

El descriptor <symbol>.frame nombra el módulo, la función y la aridad, y contiene el puntero al código del cuerpo y los recuentos de ranuras (abi). Las palabras de cabecera no son términos; un recorrido sigue previous y lee los recuentos de ranuras de los descriptores. Un marco inferior propiedad del runtime se sitúa bajo la primera llamada de cada proceso y de cada invocación del anfitrión: retornar a él guarda el resultado en el primer registro y termina la llamada nativa.

Operaciones

Cada llamada apila un marco; las funciones hoja no lo omiten, de modo que cada función conserva su entrada en la traza de pila.

Visibilidad de las raíces

En cada transferencia y en cada punto seguro, las raíces de un proceso son: las ranuras de términos 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 (raíces).

Los valores nunca sobreviven a una transferencia en registros nativos ni en valores SSA, salvo los parámetros de la propia transferencia. Cada cuerpo vuelve a cargar la dirección de su marco desde stack + frame después de 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 guardado en valores SSA; la regla de recarga para las recolecciones está fijada en regla de recarga.

El bloque de pila crece duplicándose y moviéndose mientras el límite de todo el runtime lo permita, nunca se reduce durante la ejecución y está separado del bloque de heap. No tiene límite de número de marcos; las palabras de la pila cuentan para el límite de memoria del runtime.

Destinos

musttail con una firma de código uniforme 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 de un error cuando no puede respetar una llamada musttail, de modo que una compilación correcta es la garantía. Alternativa para un destino futuro que lo rechace: un trampolín, donde cada código devuelve el siguiente puntero a código al bucle del planificador en lugar de saltar; los marcos, las raíces y los índices de continuación no cambian. Ningún destino requerido lo necesita.

Implementación

Alternativas comparadas

tests/prototypes/execution_model hace manualmente el lowering de las mismas funciones Erlang (sum/1 con recursión de cuerpo, loop/2 con recursión de cola, fail/1 que lanza una excepción a profundidad N, catcher/1 que la captura) de tres maneras y las compila para todos los destinos anteriores en O0 y O2 (python tests/prototypes/execution_model/run.py). En Windows x64 en O2, el modelo elegido ejecutó una recursión de cuerpo de 1M de profundidad en 17 ms con 5 palabras por marco y 10M de llamadas de cola con pila constante; su variante de trampolín superó las mismas ejecuciones. Rechazadas:

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