Benchmarks, measured

Rendered from benchmarks/poly/README.md (in Spanish): the repository's polyglot bench (14 programs × 9 languages, same output verified by checksum), reproducible with bench.py. The chronicle of every optimization arc lives in PERFORMANCE.md.

loopsum 29.9 ms

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

fibrec 19.5 ms

raylang1.00×
rustc -O0.81×
go0.89×
node2.10×

wordcount 40.3 ms

raylang1.00×
rustc -O1.55×
go1.18×
node3.22×

jsonserialize 32.1 ms

raylang1.00×
rustc -O0.88×
go0.90×
node2.35×

jsondeserialize 79.2 ms

raylang1.00×
rustc -O0.63×
go0.59×
node1.22×

logparse 23.9 ms

raylang1.00×
rustc -O1.36×
go1.00×
node2.17×

treealloc 19.6 ms

raylang1.00×
rustc -O1.47×
go1.56×
node1.12×

sortnums 20.6 ms

raylang1.00×
rustc -O1.18×
go3.85×
node22.68×

matrixmul 6.5 ms

raylang1.00×
rustc -O0.99×
go1.33×
node4.05×

regex 68.4 ms

raylang1.00×
rustc -O0.40×
go1.19×
node0.97×

Resultados (22 sep 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.

Programa Qué mide
empty programa vacío
print un 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.

Programa Qué mide
loopsum suma con módulo en un loop de 10M iteraciones
fibonacci fibonacci recursivo para n=0..9 — llamadas a función, poco trabajo
fibrec fibonacci recursivo profundo fib(34) — stress de llamadas a función
factorial factorial 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.

Programa Modela Primitivas ejercitadas
wordcount Crunch de datos: contar frecuencias sobre líneas sintéticas split + hash map get/insert + sort (agregación)
jsonserialize Ruta de salida: serializar N registros a JSON to_string + concatenación + push/join (construcción de strings)
jsondeserialize Ruta de entrada, contraparte de jsonserialize: parsear los registros búsqueda de substrings (index_of/substring) + parse_int, sin librería de JSON
logparse Ruta de entrada: parsear N líneas de log split + parse_int + agregación en dos maps

Estructuras de datos / GC

Programa Qué mide
treealloc Presió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

Programa Qué mide
sortnums Ordenar 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).
matrixmul Multiplicació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

Programa Qué mide
regex Extracció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 (22 sep 2026 — M3 Pro, mediana de 10 corridas, 5 de calentamiento)

Corridas archivadas completas (export de bench.py --export-md, todas las tablas y el entorno): results/2026-09-22-v1.27.5.md — las 10 variantes en las mismas corridas, ray = release 1.27.5 tal como se publica (build plano) — y results/2026-09-22-v1.27.5-pgo.md — solo ray y native, con ray = build PGO del mismo commit (make pgo, docs/build.md §3). Las columnas native/vs node/vs go/ vs rustc -O salen de la primera; la columna VM ÷ native, de la segunda. Referencias anteriores: results/2026-09-10-v1.14.0.md y results/2026-07-30-arco-vm.md.

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):

Programa native vs node vs go vs rustc -O VM ÷ native
loopsum 29.9 ms 🥇 9.01× 1.00× (empate) 1.00× (empate) 13.9×
fibrec 19.5 ms 2.10× 0.89× 0.81× 28×
wordcount 40.3 ms 🥇 3.22× 1.18× 1.55× 4.0×
jsonserialize 32.1 ms 2.35× 0.90× 0.88× 2.6×
jsondeserialize 79.2 ms 1.22× 0.59× 0.63× 3.1×
logparse 23.9 ms 🥇 2.17× 1.00× (empate) 1.36× 2.8×
treealloc 19.6 ms 🥇 1.12× 1.56× 1.47× 26×
sortnums 20.6 ms 🥇 22.68× 3.85× 1.18× 6.5×
matrixmul 6.5 ms 🥇 4.05× 1.33× 0.99× (empate) 2.8×
regex 68.4 ms 0.97× 1.19× 0.40× 4.0×

Con qué build se mide la VM. La columna VM ÷ native (y todo lo que dice «VM» en este documento) se mide con un build PGO de ray (make pgo; guía en docs/build.md §3). La release publicada en GitHub es un build plano: en la VM rinde entre un 10 % y un 26 % menos en cómputo puro (medido el 22 sep 2026, mismo commit y misma máquina: loopsum 473 → 395 ms, fibrec 721 → 532 ms, sortnums 157 → 118 ms con PGO). El binario nativo (ray build --native) no depende del build de la toolchain: lo compila rustc con su propio LTO, y entre ambas corridas varía solo el ruido de máquina (2–11 %). Las razones frente a node, Go y rustc -O son, por tanto, las que obtiene cualquiera con la release.

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

Script Qué mide Cómo
build-all.sh — Compila todos los .ray (ray build --native), .go (go build) y .rs (rustc -O) de cada subdirectorio
bench.py Tiempo + memoria en un solo set de corridas time.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.py Tiempo + memoria + ranking, interactivo Interfaz de texto con curses: selector de programa/variantes/corridas con teclado, progreso en vivo y gráficos de barras finales
benchlib.py — Módulo compartido: descubrimiento de programas/variantes, formato de tabla, export a Markdown/CSV, config
settings.toml — Activa/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).