Benchmarks medidos

Renderizado de benchmarks/poly/README.md: el banco poliglota del repositorio (14 programas × 9 lenguajes, mismo output verificado por checksum), reproducible con bench.py. La crónica de cada arco de optimización vive en PERFORMANCE.md.

loopsum 27.3 ms

raylang1.00×
rustc -O1.00×
go1.01×
node9.06×

fibrec 17.7 ms

raylang1.00×
rustc -O0.79×
go0.91×
node2.21×

wordcount 38.4 ms

raylang1.00×
rustc -O1.60×
go1.16×
node3.28×

jsonserialize 28.6 ms

raylang1.00×
rustc -O0.92×
go0.96×
node2.50×

jsondeserialize 74.4 ms

raylang1.00×
rustc -O0.66×
go0.60×
node2.11×

logparse 21.5 ms

raylang1.00×
rustc -O1.49×
go1.05×
node2.38×

treealloc 18.1 ms

raylang1.00×
rustc -O1.52×
go1.59×
node1.13×

sortnums 18.0 ms

raylang1.00×
rustc -O1.12×
go3.51×
node19.99×

matrixmul 5.6 ms

raylang1.00×
rustc -O1.01×
go1.35×
node4.12×

regex 65.2 ms

raylang1.00×
rustc -O0.40×
go1.17×
node0.95×

Resultados (29 jul 2026 — M3 Pro, mediana de 10 corridas, 5 de calentamiento) · el mismo output en todas las variantes, verificado por checksum · barra más larga = más lento que el binario nativo de raylang (1×), truncada en 4×.


Benchmarks poliglotas

14 programas, cada uno implementado en 9 variantes de lenguaje (js, lua, php, pl, py, ray, rb, go, rust) — más los 3 binarios compilados (nativo de Raylang, Go, Rust) — 12 archivos por directorio en total, verificados a que produzcan el mismo output determinista (mismo checksum, comparado por diff). Todos son autocontenidos (generan sus propios datos, sin ficheros ni red): así el benchmark aísla el coste de cómputo del lenguaje, no la varianza del disco/SO.

Cada programa vive en su propio directorio (wordcount/, jsonserialize/, treealloc/, …) y se agrupa por categoría (ver tabla abajo) — el mismo agrupamiento que ves en ./bench.py list y en el menú de ./tui.py. Se ejecutan con el arnés poliglota en Python (ya no depende de hyperfine; usa time.perf_counter internamente y presenta los resultados en una tabla):

./bench.py wordcount        # o cualquier otro programa
./bench.py all              # todos
./bench.py list             # listado agrupado por categoría, con descripción

Los programas, agrupados por qué miden

Arranque

Overhead puro de arrancar el runtime/intérprete, sin cómputo real — el piso de latencia de cada lenguaje.

ProgramaQué mide
emptyprograma vacío
printun solo print — arranque + I/O mínimo

CPU / aritmética-recursión

Aritmética y llamadas a función puras, sin tocar la stdlib — donde raylang, sin JIT ni backend nativo, rinde peor relativo a los demás.

ProgramaQué mide
loopsumsuma con módulo en un loop de 10M iteraciones
fibonaccifibonacci recursivo para n=0..9 — llamadas a función, poco trabajo
fibrecfibonacci recursivo profundo fib(34) — stress de llamadas a función
factorialfactorial recursivo para n=0..9 — recursión simple

Datos de servicio (stdlib string + hashmap)

El trabajo real de un servicio (I/O/datos-bound): parsear peticiones, agregar en tablas hash y construir respuestas — ejercita la stdlib, no la aritmética.

ProgramaModelaPrimitivas ejercitadas
wordcountCrunch de datos: contar frecuencias sobre líneas sintéticassplit + hash map get/insert + sort (agregación)
jsonserializeRuta de salida: serializar N registros a JSONto_string + concatenación + push/join (construcción de strings)
jsondeserializeRuta de entrada, contraparte de jsonserialize: parsear los registrosbúsqueda de substrings (index_of/substring) + parse_int, sin librería de JSON
logparseRuta de entrada: parsear N líneas de logsplit + parse_int + agregación en dos maps

Estructuras de datos / GC

ProgramaQué mide
treeallocPresión de GC/allocator: construir, contar y descartar muchos árboles binarios pequeños (benchmark clásico "binary-trees"). Árboles completos, profundidad 4 a 15 (min_depth=4, max_depth=14, +stretch de 15). Sin hashmaps de por medio — mide alocación pura.

Numérico

ProgramaQué mide
sortnumsOrdenar un array numérico grande (no claves de string como en wordcount/logparse): 1 000 000 de enteros generados con un LCG determinista (MINSTD, multiplicador 48271 mod 2³¹-1).
matrixmulMultiplicación de matrices 200×200 — el único workload con punto flotante (todo lo demás en la suite es entero puro). Valores deterministas (i·n+j) mod 13 / (j·n+i) mod 17.

Pattern matching

ProgramaQué mide
regexExtracción de campos con motor de regex (distinto del split ingenuo de logparse): 200 000 líneas userN GET /api/M STATUS MSms, patrón ^user(\d+) GET /api/(\d+) (\d+) (\d+)ms$.

Nota sobre regex: regex.rs parsea a mano el mismo patrón fijo porque el crate regex de Rust requeriría Cargo + red, rompiendo el build de un solo archivo con rustc usado para el resto de la suite (con el crate, Rust puro cuesta ~49 ms, no ~26). regex.ray usa el motor REAL de la stdlib (import std/regex; con compile() + trait Matcher): desde R5/R7 (PERFORMANCE.md Fases 63 y 69) tanto el binario nativo como la VM lo despachan al crate regex vía ray-runtime — con la validación y el dialecto de la librería raylang —, así que la variante compite motor contra motor. La Pike VM escrita en raylang (la implementación de referencia) sigue siendo el motor del intérprete (--interp) y de los builds slim.

Resultados (29 jul 2026 — M3 Pro, mediana de 10 corridas, 5 de calentamiento)

Corrida archivada completa (export de bench.py --export-md, todas las tablas y el entorno): results/2026-07-30-arco-vm.md — la referencia post-arco de la VM (Fases 69-75) con el arnés corregido, todas las filas 10/10.

Suite completa con el arnés actual (auto-medición: los 12 programas de cómputo cronometran su propio workload, así que no cuentan el arranque del runtime; empty/print sí lo miden, que es justo lo que evalúan). ray = la VM; native = ray build --native --release, que desde el arco F enlaza fibras M:N por defecto.

Tiempo del binario nativo frente a los compilados y a node (× = cuántas veces más lento que native; <1 significa que nos gana):

Programanativevs nodevs govs rustc -OVM ÷ native
loopsum27.3 ms 🥇9.06×1.01×1.00×13×
fibrec17.7 ms2.21×0.91×0.79×26×
wordcount38.4 ms 🥇3.28×1.16×1.60×4.6×
jsonserialize28.6 ms2.50×0.96×0.92×2.4×
jsondeserialize74.4 ms2.11×0.60×0.66×3.4×
logparse21.5 ms 🥇2.38×1.05×1.49×3.0×
treealloc18.1 ms 🥇1.13×1.59×1.52×25×
sortnums18.0 ms 🥇19.99×3.51×1.12×6.0×
matrixmul5.6 ms 🥇4.12×1.35×1.01× (empate)2.7×
regex65.2 ms0.95×1.17×0.40×4.0×

Las filas de matrixmul (Fase 67: hoist de borrows → vectorización) y de wordcount/ jsondeserialize/logparse/regex (Fase 68: fusiones de substring/split + el borde de capturas) son las re-mediciones del mismo día con el arnés, tras esos arcos del transpilador; la corrida original está en el historial de PERFORMANCE.md. La columna VM ÷ native es del 29–30 jul, tras los arcos de la VM (Fases 69–72: regex sobre el crate, kernel DotRange, ronda 5 de superinstrucciones y el despacho inlineado) y con la corrección del sesgo de presupuesto del arnés (Fase 73).

Ranking combinado (tiempo × memoria, media geométrica): el binario nativo queda #1 o #2 en 11 de los 12 programas (#1 absoluto en wordcount y logparse) y #3 en el restante (jsonserialize) — nunca por debajo del tercer puesto, contra 9 lenguajes.

Lectura

Reproducir

# compilar primero los binarios nativos (.ray), Go (.go) y Rust (.rs) de cada directorio:
./build-all.sh

# tiempo Y memoria de los tres, con export a markdown (una sola pasada: cada
# corrida cronometra y mide RSS a la vez, así ambas tablas son de las MISMAS
# corridas):
./bench.py wordcount --export-md /tmp/wc.md
./bench.py jsonserialize --export-md /tmp/js.md
./bench.py logparse --export-md /tmp/lp.md

Scripts

ScriptQué mideCómo
build-all.shCompila todos los .ray (ray build --native), .go (go build) y .rs (rustc -O) de cada subdirectorio
bench.pyTiempo + memoria en un solo set de corridastime.perf_counter alrededor de un spawn directo y ru_maxrss del wait4 de ESA misma corrida; tablas con mediana/mín/máx/MAD/ratio más un ranking combinado
tui.pyTiempo + memoria + ranking, interactivoInterfaz de texto con curses: selector de programa/variantes/corridas con teclado, progreso en vivo y gráficos de barras finales
benchlib.pyMódulo compartido: descubrimiento de programas/variantes, formato de tabla, export a Markdown/CSV, config
settings.tomlActiva/desactiva variantes de lenguaje y fija defaults de --runs/--warmup, de forma persistente

Metodología de medición

build-all.sh compila en paralelo (un job por core): compilar no es medir, ahí el multicore es gratis. Las corridas medidas son SIEMPRE seriales — variantes concurrentes se pelean por caché/memoria/térmica y en Apple Silicon caen en E-cores: velocidad a cambio de exactitud, no.

Consejos de higiene al correr: equipo enchufado (sin Low Power Mode), sin compilaciones ni apps pesadas en paralelo, y descartar la primera sesión tras un reboot (caches fríos).

bench.py (línea de comandos) y tui.py (interactivo) comparten el mismo arnés (benchlib.py) y la misma interfaz:

./bench.py                 # lista los programas y pregunta cuál medir
./bench.py list            # solo lista
./bench.py <n>             # por número del listado
./bench.py <nombre>        # todas las variantes de <nombre>
./bench.py all             # todos los programas

# flags: --runs N --warmup N --exclude "a b c" --export-md FILE --export-csv FILE
#        --prepare CMD (o --clean, atajo de --prepare "sync")

Interfaz interactiva (TUI)

./tui.py levanta una interfaz de texto (solo stdlib, curses — sin dependencias que instalar) para no tener que memorizar flags:

./tui.py

El menú principal es solo elegir programa (o "ejecutar todos") — enter corre el benchmark directo, sin pasos intermedios. La esquina superior derecha muestra un panel de configuración persistente (20r/10w · 7/10 lenguajes); presionando s en cualquier momento se abre la pantalla de Configuración (lenguajes a incluir, corridas, calentamiento), que queda guardada para las corridas siguientes hasta que se vuelva a cambiar o se cierre la TUI.

Al elegir un programa: corre con barras de progreso en vivo por variante → pantalla de resultados con gráficos de barras (tiempo, memoria) coloreados por puesto (verde = mejor, rojo = peor) y la tabla de ranking combinado, con scroll (/) si no entra en la terminal. q/esc vuelve un paso atrás en cualquier pantalla; desde el menú principal sale del programa.

Activar/desactivar lenguajes

settings.toml (raíz del proyecto) controla qué variantes participan en list/choose/bench, de forma persistente (sin repetir --exclude en cada corrida):

native = true   # binario nativo (ray build --native)
go     = true   # binario compilado (go build)
rs     = true   # binario compilado (rustc -O)
js     = true   # node
lua    = true   # lua
php    = true   # php
pl     = true   # perl
py     = true   # python3
ray    = true   # ray run (intérprete)
rb     = true   # ruby

# Defaults de corridas/calentamiento (se pueden sobreescribir con --runs/--warmup).
[bench]
runs   = 20   # corridas medidas por variante
warmup = 10   # corridas de calentamiento descartadas

Poner una clave de lenguaje en false la excluye en bench.py y en tui.py; se combina con --exclude si también se pasa por línea de comandos. Un lenguaje habilitado cuyo intérprete no esté instalado en la máquina no rompe la corrida: variants_for lo omite con un aviso, así que el default commiteado deja los nueve activos y cada máquina mide lo que tiene. La tabla [bench] fija los valores por defecto de --runs/--warmup; un flag explícito en la línea de comandos siempre gana sobre el config. Si el archivo no existe, todas las variantes quedan habilitadas y se usan los defaults de fábrica (20 corridas, 10 de calentamiento).