LLVM y rendimiento
¿Qué pasa cuando compilas?
En las Partes 1 y 2, escribiste código Nyx, lo compilaste y lo ejecutaste. Pero ¿qué pasa realmente entre tu archivo .nx y el programa en ejecución? Entender este pipeline te ayuda a escribir código más rápido y depurar problemas de rendimiento.
El pipeline de compilación
Cuando ejecutas make run FILE=programa.nx, ocurren cuatro cosas:
programa.nx → Compilador Nyx → programa.ll → Clang/LLVM → programa (binario)
(Nyx) (parser + (LLVM IR) (optimizador + (código máquina
codegen) ensamblador) nativo)
- El Compilador Nyx lee tu archivo
.nx, lo parsea en un AST (Árbol de Sintaxis Abstracta), verifica tipos y genera LLVM IR — una representación intermedia. - LLVM toma el IR, lo optimiza agresivamente y genera código máquina nativo para tu CPU.
El resultado es un binario independiente — sin intérprete, sin VM, sin overhead de runtime. Solo instrucciones de máquina.
¿Qué es LLVM IR?
LLVM IR es un lenguaje de bajo nivel que se ubica entre Nyx y el código máquina. Se parece a assembly pero es portable entre arquitecturas de CPU. Así se ve una función simple:
fn sumar(a: int, b: int) -> int { return a + b }
Se convierte en este LLVM IR:
define i64 @sumar(i64 %a, i64 %b) {
entry:
%result = add i64 %a, %b
ret i64 %result
}
No necesitas escribir IR — el compilador de Nyx lo genera. Pero verlo te ayuda a entender en qué se convierte tu código.
Ver el IR
Para ver el IR de cualquier programa Nyx:
make compile FILE=programa.nx cat programa.ll
Esto es útil para entender el rendimiento. Si una función genera cientos de instrucciones IR, podría valer la pena simplificarla.
Cómo LLVM optimiza tu código
LLVM aplica docenas de pasadas de optimización. Estas son las de mayor impacto:
Eliminación de código muerto
fn main() { let x: int = 42 // se computa pero nunca se usa let y: int = 10 print(y) // solo y importa }
LLVM elimina x completamente. El programa compilado nunca computa 42.
Plegado de constantes
fn main() { let resultado: int = 3 * 4 + 5 print(resultado) // 17 }
LLVM computa 17 en tiempo de compilación. El binario solo carga la constante.
Optimización de bucles
fn sumar_hasta(n: int) -> int { var total: int = 0 var i: int = 0 while i < n { total += i i += 1 } return total }
LLVM puede desenrollar bucles pequeños, vectorizar operaciones y a veces reemplazar el bucle entero con una fórmula cerrada.
Inlining
Las funciones pequeñas se incrustan — su código se copia directamente en el llamador, eliminando el overhead de la llamada:
fn cuadrado(x: int) -> int { return x * x } fn main() { let n: int = cuadrado(5) // se convierte en: let n: int = 5 * 5 }
Características de rendimiento de Nyx
Como Nyx compila a código nativo via LLVM, logra rendimiento comparable a C:
| Benchmark | Nyx | C | Ratio |
|---|---|---|---|
| fibonacci(40) | 166ms | 190ms | 0.87x (Nyx gana) |
| primes(100K) | 3.6ms | 3.6ms | 1.0x |
| loop(100M) | 0μs | 0μs | ambos optimizados |
| map(100K ops) | 24.6ms | 23ms | 1.07x |
| HTTP req/s | 73,863 | — | competitivo con Go |
Escribir código Nyx rápido
Preferir enteros sobre strings
Las operaciones con enteros son una sola instrucción de CPU. Las operaciones con strings asignan memoria y copian bytes:
// Rápido — comparación de enteros if estado == 200 { ... } // Más lento — comparación de strings (compara byte por byte) if texto_estado == "OK" { ... }
Minimizar asignaciones en bucles calientes
Cada vez que creas un string, array o struct, el recolector de basura debe rastrearlo. En un bucle ajustado, esto se acumula:
// Lento — crea un nuevo string cada iteración var i: int = 0 while i < 1000000 { let s: String = "hola" + int_to_string(i) // ¡asignación! i += 1 } // Rápido — evitar asignaciones innecesarias var i: int = 0 var total: int = 0 while i < 1000000 { total += i // sin asignación i += 1 }
Usar StringBuilder para construir strings
Cuando construyas strings en un bucle, usa StringBuilder en lugar de concatenación:
// Lento — O(n²) porque cada + crea un nuevo string var resultado: String = "" var i: int = 0 while i < 10000 { resultado = resultado + "x" i += 1 } // Rápido — O(n) con StringBuilder var sb: StringBuilder = sb_new() var i: int = 0 while i < 10000 { sb_append(sb, "x") i += 1 } let resultado: String = sb_to_string(sb)
Cachear computaciones repetidas
// Lento — llama length() cada iteración while i < arr.length() { // ... i += 1 } // Rápido — cachear la longitud let len: int = arr.length() while i < len { // ... i += 1 }
El recolector de basura
Nyx usa el Boehm GC — un recolector de basura conservativo. Libera automáticamente la memoria que ya no usas. Nunca necesitas llamar free() (a menos que optes por gestión manual de memoria unsafe).
El GC se ejecuta periódicamente. Escanea la memoria para encontrar objetos que ya no están referenciados y los reclama. Esto introduce pequeñas pausas, pero Nyx ajusta el GC para baja latencia:
- Recolección incremental reduce tiempos de pausa
- Frecuencia de recolección reducida para cargas de servidor
- La mayoría de las asignaciones son de corta vida y se recolectan rápido
Para la gran mayoría de programas, el GC es invisible. Para necesidades extremas de rendimiento (sistemas en tiempo real, motores de juegos), Nyx ofrece modo sin GC:
make run-no-gc FILE=programa.nx
En modo sin GC, debes gestionar la memoria manualmente con alloc() y free().
Self-hosting: el benchmark definitivo
El compilador de Nyx está escrito en Nyx y se compila a sí mismo. Esto se llama self-hosting — y es la prueba definitiva de rendimiento. Si el compilador fuera lento, compilarse a sí mismo sería dolorosamente lento.
El pipeline de compilación logra un punto fijo: compilar el compilador dos veces produce salida idéntica. Esto prueba tanto corrección como consistencia.
Ejercicios
- Escribe un programa que sume números del 1 al 10,000,000. Compílalo con
make compiley mira el archivo.llgenerado. ¿Puedes encontrar el bucle en el IR?
- Escribe dos versiones de construcción de strings: una con concatenación
+en un bucle, otra conStringBuilder. Mide el tiempo de ambas con un gran número de iteraciones.
- Escribe una función que compute fibonacci(40). Compara el tiempo con el mismo algoritmo en Python u otro lenguaje interpretado.
- Mira el IR de una función simple con un
if. ¿Puedes identificar las instrucciones de salto?
- Escribe un programa que cree 1 millón de structs en un bucle. Luego escribe uno que cree 1 millón de enteros. Compara la diferencia de rendimiento.
Resumen
- Nyx compila a LLVM IR, luego LLVM genera código máquina nativo optimizado.
- Usa
make compilepara ver el IR generado. - LLVM optimiza: eliminación de código muerto, plegado de constantes, inlining, optimización de bucles.
- Nyx logra rendimiento similar a C en la mayoría de benchmarks.
- Escribe código rápido: prefiere enteros, minimiza asignaciones, usa StringBuilder, cachea valores.
- El Boehm GC maneja la memoria automáticamente con pausas de baja latencia.
- El modo sin GC está disponible para gestión manual de memoria.
- El compilador de Nyx es self-hosting — se compila a sí mismo.
Siguiente capítulo: FFI — Llamar código C →