ADR 0012 — Mapa de lenguajes por tipo de tarea y escalera de optimización con evidencia
- Estado: aceptado (los umbrales se recalibran con el benchmark de la Ronda 1)
- Fecha: 2026-10-16
- Fuente: Fase 1, informe 22 y
benchmarks/results.md(186 mediciones, 31 implementaciones, 0 fallas, 0 no equivalentes; contenedor con 7 CPU / 22 GiB, host con otros procesos); informes 23 y 26
Contexto
La especificación proponía un mapa de lenguajes a validar con benchmark: wrangling en DuckDB/arrow desde R o Python, inferencia con diseño muestral en R, ML en Python, simulación numérica en Julia “que sustituye a C++ salvo que un benchmark muestre lo contrario”. El ítem 22 corrió dos tareas reproducibles dentro de la imagen sobre ENDES 2024 módulo 1629:
| Tarea | Implementación | Mediana x1 (135 k filas reales) | Mediana x25 (3,4 M filas, réplica sintética) |
|---|---|---|---|
| Join ponderado + agregación | polars (Python) | 0,044 | 0,19 |
| DuckDB (Python) | 0,13 | 0,39 | |
| DuckDB SQL desde R | 0,15 | 0,43 | |
| arrow perezoso (R) | 0,26 | 0,43 | |
| DuckDB vía dbplyr (R) | 0,42 | 0,70 | |
| Julia DataFrames | 0,03 (+ 6–9 s de JIT por proceso) | 0,79 | |
| data.table (R) | 0,16 | 1,05 | |
| dplyr en memoria (R) | 0,13 | 1,69 | |
| Microsimulación 1e6 × 10 pasos | Julia multihilo (7 hilos) | — | 0,06 (calentamiento 0,16 s) |
| Julia bucle (1 hilo) | — | 0,21 | |
| pyarrow.compute | — | 0,28 | |
| Rcpp (1 hilo) | — | 0,33 | |
| R vectorizado | — | 0,74 | |
| DuckDB SQL | — | 1,04 |
A la escala real del módulo (x1) dplyr en memoria iguala o supera a DuckDB y arrow, y Julia es la más rápida una vez compilada: la escalera solo paga a partir de ~1 M de filas (revisión adversarial M8).
Las 18 variantes del join coinciden (diferencia relativa ≤ 4e-13); las de microsimulación pasan pruebas z y χ² contra la expectativa analítica (la tolerancia 1e-3 no sirve como criterio: es del orden del error Monte Carlo). Diferencias < 1,5× no son concluyentes en este host.
Alternativas
- Julia por defecto para todo lo numérico: descartado; pierde 4× frente a polars en joins y paga 6–9 s de compilación por proceso.
data.tablecomo motor de wrangling: descartado; no supera a DuckDB ni a arrow en estas tareas.- Rcpp como primer salto de optimización: descartado como regla general; arrow/DuckDB perezoso rinde más sin salir de R para datos tabulares.
Decisión
- Mapa de lenguajes por tarea ([provisional] hasta repetir el benchmark en la Ronda 1): wrangling, joins y agregaciones → dplyr en memoria por debajo de ~1 M de filas; por encima, DuckDB o arrow desde R (SQL directo antes que
dbplyrcuando el rendimiento importe), o polars/DuckDB desde Python si la etapa ya es Python; inferencia con diseño muestral, espacial y HTA → R (survey/srvyr); predicción/ML → Python; microsimulación multihilo medida → Julia (-t auto) como proceso independiente (ADR 0011). MCMC queda sin asignar hasta un benchmark propio (Turing.jl frente acmdstanr): a un hilo la ventaja de Julia sobre Rcpp (1,5×) está dentro del ruido y el 5,5× compara 7 hilos contra 1 (revisión adversarial M9). - Escalera de optimización con evidencia: dplyr en memoria → arrow/DuckDB perezoso con
collect()tardío → Rcpp encapsulado tras una función R con test de equivalencia → proceso Python o Julia. Cada salto exigebench::mark(≥ 3 repeticiones, mediana) más RSS pico, un test de equivalencia numérica y un ADR corto que cite las cifras; sin eso, no se cambia de peldaño. - Legibilidad al optimizar: la interfaz pública siempre es una función R documentada con roxygen2; el C++ o el script externo quedan encapsulados; un test
testthatcompara la implementación rápida con la de referencia (bit-idéntica cuando el RNG lo permite; z y χ² cuando no). - Medición estándar:
bench::mark(memory = TRUE)para R,tracemallocpara Python,@timedpara Julia, y RSS pico del proceso como única medida de memoria comparable entre lenguajes (profmemno ve las asignaciones de Arrow ni DuckDB). Cada etapa detargetsregistraseconds,bytesyrss_peak_bytesen el manifiesto de corrida. - Imagen:
data.tablese declara explícitamente enenv/r/packages.txt(ya está como dependencia transitiva 1.18.4) sin ser motor por defecto; Julia permanece en la imagen porbuild-arg(ADR 0006) y se usa solo para la decisión 1; se evalúan para la Fase 3numpy,duckplyryprofvis(ausentes hoy). Dentro de la imagen, Python se invoca conuv run --no-sync. - Umbral de arranque por tipo de etapa: una etapa en otro lenguaje solo se justifica si el ahorro medido supera su costo de arranque en la corrida completa: Python ≈ 1,1 s, R ≈ 1,3 s, Julia ≈ 1,7 s más la compilación de lo que use (0,16 s en la microsimulación; 6–9 s si carga DataFrames).
Consecuencias
docs/policies/performance.mdydocs/policies/r-style.mdoperan estas reglas; la Fase 3 añade un lint que exige ADR enlazado en todo PR que cambie de motor o lenguaje.benchmarks/run_all.Rse conserva como benchmark de regresión y se vuelve a correr en cada cambio de imagen (comparación contrabenchmarks/results.md).- Provisional: cifras medidas en un host ruidoso y con Rcpp en un hilo (el RNG de R no es multihilo); la Ronda 1 repite el benchmark en el runner de CI y, si la diferencia Julia/Rcpp cae dentro del ruido, Julia queda restringida a MCMC.