Apéndice A: LuaJIT - El Turbo de Lua
Apéndice A: LuaJIT - El Turbo
“Make it work, make it right, make it fast.” — Kent Beck
LuaJIT es una implementación alternativa de Lua con un compilador JIT (Just-In-Time) que puede hacer tu código 10-100x más rápido que Lua estándar. Es ampliamente usado en producción: Nginx (OpenResty), Redis scripting, videojuegos, y más.
¿Qué es LuaJIT?
LuaJIT es un drop-in replacement para Lua 5.1 (con algunas extensiones de 5.2/5.3). El código escrito para Lua estándar generalmente funciona sin cambios en LuaJIT.
Ventajas de LuaJIT
- Velocidad extrema: 10-100x más rápido que Lua 5.x
- FFI library: Llamar código C sin escribir bindings
- Compatible: Casi 100% compatible con Lua 5.1
- Mismo footprint: Tamaño similar a Lua estándar
- Battle-tested: Usado en producción por millones
Limitaciones
- Stuck en 5.1: No soporta características de Lua 5.3/5.4
- Plataformas: x86/x64, ARM. No todas las arquitecturas
- GC64: Problemas con >2GB de datos en algunas plataformas (resuelto en 2.1+)
- Mantenimiento: Proyecto principalmente mantenido por Mike Pall (un solo desarrollador)
Instalación
# macOS con Homebrew
brew install luajit
# Ubuntu/Debian
sudo apt-get install luajit
# Desde source
git clone https://luajit.org/git/luajit.git
cd luajit
make && sudo make install
Verificar instalación:
luajit -v
# LuaJIT 2.1.0-beta3 -- Copyright (C) 2005-2022 Mike Pall
Diferencias con Lua Estándar
Extensiones de LuaJIT
1. table.new(narray, nhash)
Prealoca memoria para tablas:
-- LuaJIT solamente
local t = require("table.new")(100, 0) -- 100 array slots, 0 hash slots
-- Lua estándar: no hay preallocación
local t = {}
for i = 1, 100 do
t[i] = i
end
Performance:
- LuaJIT con
table.new: Una sola allocación - Lua estándar: Múltiples reallocaciones conforme crece
2. table.clear(t)
Vacía una tabla sin deallocar memoria:
local t = {1, 2, 3, 4, 5}
-- >>> table.clear(t)
-- >>> print(#t)
-- 0
-- La memoria sigue allocada, perfecto para reutilización
3. bit library
Operaciones bitwise rápidas:
local bit = require("bit")
-- >>> print(bit.bor(1, 2)) -- OR
-- 3
-- >>> print(bit.band(6, 3)) -- AND
-- 2
-- >>> print(bit.lshift(1, 3)) -- Shift left
-- 8
Lua 5.3+ tiene operadores nativos (|, &, <<), pero LuaJIT provee la library para 5.1.
FFI: Foreign Function Interface
La killer feature de LuaJIT. Permite llamar funciones C directamente sin escribir bindings.
Ejemplo Básico
local ffi = require("ffi")
-- Declarar funciones C
ffi.cdef[[
int printf(const char *fmt, ...);
unsigned int sleep(unsigned int seconds);
]]
-- Llamarlas directamente
ffi.C.printf("Hello from C!\n")
ffi.C.printf("Number: %d\n", 42)
ffi.C.sleep(1)
Structs C
local ffi = require("ffi")
ffi.cdef[[
typedef struct {
double x;
double y;
} point_t;
]]
-- Crear instancia
local point = ffi.new("point_t", {10.5, 20.3})
-- >>> print(point.x, point.y)
-- 10.5 20.3
-- Modificar
point.x = 100
point.y = 200
Arrays C
local ffi = require("ffi")
-- Array de 1000 doubles
local arr = ffi.new("double[?]", 1000)
-- Llenar el array
for i = 0, 999 do
arr[i] = i * 1.5
end
-- >>> print(arr[500])
-- 750.0
Nota: FFI arrays usan índices desde 0, no 1.
Caso Práctico: Binding a zlib
local ffi = require("ffi")
-- Cargar librería
local zlib = ffi.load("z")
-- Declarar funciones
ffi.cdef[[
unsigned long compressBound(unsigned long sourceLen);
int compress2(unsigned char *dest, unsigned long *destLen,
const unsigned char *source, unsigned long sourceLen,
int level);
int uncompress(unsigned char *dest, unsigned long *destLen,
const unsigned char *source, unsigned long sourceLen);
]]
-- Wrapper Lua
local Compress = {}
function Compress.compress(data, level)
level = level or 6 -- Default compression level
local src_len = #data
local dest_len = zlib.compressBound(src_len)
local dest = ffi.new("unsigned char[?]", dest_len)
local dest_len_ptr = ffi.new("unsigned long[1]", dest_len)
local result = zlib.compress2(
dest, dest_len_ptr,
ffi.cast("const unsigned char*", data), src_len,
level
)
if result ~= 0 then
error("Compression failed: " .. result)
end
return ffi.string(dest, dest_len_ptr[0])
end
function Compress.decompress(compressed_data, original_size)
local dest = ffi.new("unsigned char[?]", original_size)
local dest_len = ffi.new("unsigned long[1]", original_size)
local result = zlib.uncompress(
dest, dest_len,
ffi.cast("const unsigned char*", compressed_data),
#compressed_data
)
if result ~= 0 then
error("Decompression failed: " .. result)
end
return ffi.string(dest, dest_len[0])
end
return Compress
Uso:
local Compress = require("compress")
local original = "Hello World! " .. string.rep("Lua is awesome! ", 100)
local compressed = Compress.compress(original, 9)
print("Original size: " .. #original)
print("Compressed size: " .. #compressed)
print("Ratio: " .. math.floor(#compressed / #original * 100) .. "%")
local decompressed = Compress.decompress(compressed, #original)
assert(decompressed == original)
Optimizaciones JIT
Cómo Funciona el JIT
LuaJIT compila hot loops (loops ejecutados frecuentemente) a código máquina nativo.
- Tracing: LuaJIT monitorea qué código se ejecuta frecuentemente
- Recording: Cuando un loop es “hot”, graba un trace
- Optimization: Optimiza el trace (inline, constant folding, etc.)
- Assembly: Compila a código máquina x86/ARM
- Execution: Ejecuta el código nativo directamente
Ver el JIT en Acción
local jit = require("jit")
-- Activar dump de traces
jit.dump.on("t")
-- Código con hot loop
local sum = 0
for i = 1, 1000000 do
sum = sum + i
end
print(sum)
Tips de Optimización
1. Evitar NYI (Not Yet Implemented)
Algunas operaciones no están optimizadas por el JIT:
-- ❌ NYI: pairs/next
for k, v in pairs(t) do
-- Este loop NO será JIT compilado
end
-- ✅ Optimizado: ipairs
for i, v in ipairs(t) do
-- Este loop SÍ será JIT compilado
end
2. Usar Tipos Consistentes
-- ❌ Malo: tipos mezclados
local x = 1
for i = 1, 100 do
x = x + 1
if i == 50 then
x = "string" -- ¡Cambio de tipo! Mata el JIT
end
end
-- ✅ Bueno: tipo consistente
local x = 1
for i = 1, 100 do
x = x + 1
end
3. Evitar Funciones Variádicas en Hot Loops
-- ❌ Malo
local function add(...)
local sum = 0
for _, v in ipairs({...}) do
sum = sum + v
end
return sum
end
for i = 1, 1000000 do
add(1, 2, 3) -- Crea tabla cada vez
end
-- ✅ Bueno
local function add(a, b, c)
return a + b + c
end
for i = 1, 1000000 do
add(1, 2, 3)
end
Benchmarks
Fibonacci (Recursivo)
local function fib(n)
if n <= 1 then return n end
return fib(n - 1) + fib(n - 2)
end
-- Lua 5.4: ~2.5 segundos para fib(40)
-- LuaJIT: ~0.15 segundos para fib(40)
-- Speedup: ~16x
Suma de Array
local t = {}
for i = 1, 10000000 do
t[i] = i
end
local sum = 0
local start = os.clock()
for i = 1, #t do
sum = sum + t[i]
end
print("Time: " .. (os.clock() - start))
-- Lua 5.4: ~0.8 segundos
-- LuaJIT: ~0.02 segundos
-- Speedup: ~40x
Operaciones con FFI
local ffi = require("ffi")
-- Array FFI
local ffi_arr = ffi.new("double[10000000]")
for i = 0, 9999999 do
ffi_arr[i] = i
end
-- Array Lua
local lua_arr = {}
for i = 1, 10000000 do
lua_arr[i] = i
end
-- Suma FFI
local sum = 0
for i = 0, 9999999 do
sum = sum + ffi_arr[i]
end
-- Suma Lua
local sum = 0
for i = 1, #lua_arr do
sum = sum + lua_arr[i]
end
-- FFI: ~0.015 segundos
-- Lua array: ~0.02 segundos
-- Speedup: ~1.3x (pero usa menos memoria)
DEEP DIVE: Trace Abort Reasons
A veces el JIT no puede compilar un trace. Razones comunes:
-- 1. Too many attempts (loop muy corto)
for i = 1, 5 do end -- JIT ni siquiera intenta
-- 2. Bytecode too complex
-- Función con demasiadas ramas
-- 3. NYI: pairs
for k, v in pairs(t) do end
-- 4. NYI: yield
for i = 1, 100 do
coroutine.yield()
end
Ver aborts:
jit.v.on() -- Verbose mode
SOAPBOX: ¿Usar LuaJIT o Lua 5.4?
Usa LuaJIT si:
- ✅ Necesitas máxima performance
- ✅ Puedes vivir con Lua 5.1 features
- ✅ Target plataformas x86/x64/ARM
- ✅ FFI es útil para tu proyecto
Usa Lua 5.4 si:
- ✅ Necesitas features nuevas (
//,&, goto, to-be-closed vars) - ✅ Target plataformas exóticas
- ✅ Prefieres estabilidad y soporte oficial
- ✅ Generational GC es importante (5.4+)
Mi opinión: Para aplicaciones nuevas con necesidad de performance, LuaJIT es difícil de vencer. Pero Lua 5.4 tiene features de lenguaje más modernas. Si no necesitas el extra de speed, 5.4 es una mejor inversión a largo plazo.
Recursos
Documentación
Proyectos que Usan LuaJIT
- OpenResty: Web framework basado en Nginx + LuaJIT
- Redis: Scripting con LuaJIT
- Tarantool: Database NoSQL con LuaJIT
- Kong: API Gateway
- Awesome WM: Window manager