← Volver al listado de tecnologías

Apéndice A: LuaJIT - El Turbo de Lua

Por: Artiko
lualuajitjitperformanceffi

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

  1. Velocidad extrema: 10-100x más rápido que Lua 5.x
  2. FFI library: Llamar código C sin escribir bindings
  3. Compatible: Casi 100% compatible con Lua 5.1
  4. Mismo footprint: Tamaño similar a Lua estándar
  5. Battle-tested: Usado en producción por millones

Limitaciones

  1. Stuck en 5.1: No soporta características de Lua 5.3/5.4
  2. Plataformas: x86/x64, ARM. No todas las arquitecturas
  3. GC64: Problemas con >2GB de datos en algunas plataformas (resuelto en 2.1+)
  4. 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:

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.

  1. Tracing: LuaJIT monitorea qué código se ejecuta frecuentemente
  2. Recording: Cuando un loop es “hot”, graba un trace
  3. Optimization: Optimiza el trace (inline, constant folding, etc.)
  4. Assembly: Compila a código máquina x86/ARM
  5. 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:

Usa Lua 5.4 si:

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


Próximo: Apéndice B: Ecosistema y Herramientas