LuaJIT y la FFI: rendimiento cercano a C con la comodidad de un lenguaje de scripting

La interpretación de Lua estándar es rápida para un lenguaje de scripting: la VM de bytecode de Lua 5.4 supera a Python en muchos benchmarks. Pero LuaJIT va un paso más allá. LuaJIT es una implementación alternativa de Lua 5.1 con un compilador JIT (Just-In-Time) que transforma los hot paths de código Lua en código máquina nativo durante la ejecución. El resultado es un rendimiento que en bucles numéricos densos se acerca —y a veces iguala— al de C compilado con gcc -O2.

LuaJIT: historia y estado actual

LuaJIT fue creado por Mike Pall y lanzado en 2005. La versión 2.1 (actualmente en beta, con git HEAD activo) es la más moderna y la que usan OpenResty, Defold, y otros entornos de producción. LuaJIT implementa Lua 5.1, no 5.4: no tiene integers nativos ni to-be-closed variables, pero sí closures, corrutinas, metatables y todos los fundamentos del lenguaje.

El JIT de LuaJIT usa una técnica llamada tracing JIT: en lugar de compilar funciones enteras, compila trazas (secuencias lineales de bytecodes que pasan por los mismos caminos repetidamente). Esto lo hace especialmente eficaz en bucles internos donde la mayoría del tiempo se gasta.

Rendimiento: qué esperar

-- Benchmark: suma de 100 millones de enteros
-- Este código con LuaJIT 2.1 es ~30-50x más rápido que Lua 5.4

local function suma_bucle(n)
    local s = 0
    for i = 1, n do
        s = s + i
    end
    return s
end

local t0 = os.clock()
local resultado = suma_bucle(100000000)
local t1 = os.clock()

print(string.format("Resultado: %d", resultado))
print(string.format("Tiempo: %.3f segundos", t1 - t0))
-- Con LuaJIT 2.1: ~0.05s
-- Con Lua 5.4:    ~1.5s
-- Con Python 3.12: ~5-8s

La FFI: llamar C sin escribir C

La FFI (Foreign Function Interface) es la característica más exclusiva de LuaJIT. Permite cargar y llamar funciones de bibliotecas C compartidas directamente desde Lua, usando solo declaraciones de tipo en sintaxis C. No hay que escribir un módulo de extensión, no hay que compilar nada extra.

local ffi = require("ffi")

-- Declarar funciones de la libc estándar
ffi.cdef([[
    int    printf(const char *fmt, ...);
    void   *malloc(size_t size);
    void   free(void *ptr);
    size_t strlen(const char *s);
    char   *strcpy(char *dest, const char *src);
    double sin(double x);
    double cos(double x);
    double sqrt(double x);
]])

-- Llamar funciones matemáticas de libm
local libc = ffi.C   -- libc ya está cargada en el proceso
print(libc.sqrt(2.0))          -- 1.4142135623731
print(libc.sin(math.pi / 6))   -- 0.5

-- Crear y gestionar memoria manualmente
local buf = ffi.cast("char *", libc.malloc(256))
libc.strcpy(buf, "Hola desde FFI")
print(ffi.string(buf))          -- Hola desde FFI
libc.free(buf)

Cargar una biblioteca dinámica propia

local ffi = require("ffi")

-- Declarar los tipos de la biblioteca
ffi.cdef([[
    typedef struct {
        double x;
        double y;
    } Vec2;

    Vec2   vec2_add(Vec2 a, Vec2 b);
    double vec2_length(Vec2 v);
    Vec2   vec2_normalize(Vec2 v);
]])

-- Cargar la .so (Linux) o .dll (Windows)
local veclib = ffi.load("./libvec2.so")

-- Crear structs directamente en Lua
local v1 = ffi.new("Vec2", {x = 3.0, y = 4.0})
local v2 = ffi.new("Vec2", {x = 1.0, y = 2.0})

local suma   = veclib.vec2_add(v1, v2)
local longit = veclib.vec2_length(v1)

print(string.format("Suma: (%.1f, %.1f)", suma.x, suma.y))   -- (4.0, 6.0)
print(string.format("Longitud v1: %.1f", longit))             -- 5.0

Tipos de datos FFI y structs

La FFI maneja todos los tipos fundamentales de C, arrays, punteros y structs:

local ffi = require("ffi")

ffi.cdef([[
    typedef struct {
        uint8_t r, g, b, a;
    } Color;

    typedef struct {
        int      ancho;
        int      alto;
        Color   *pixels;
    } Imagen;
]])

-- Array de structs gestionado por LuaJIT
local imagen = ffi.new("Imagen")
imagen.ancho = 800
imagen.alto  = 600

-- Asignar memoria para los pixels
local n_pixels = imagen.ancho * imagen.alto
imagen.pixels = ffi.cast("Color *", ffi.C.malloc(n_pixels * ffi.sizeof("Color")))

-- Rellenar con color rojo
for i = 0, n_pixels - 1 do
    imagen.pixels[i].r = 255
    imagen.pixels[i].g = 0
    imagen.pixels[i].b = 0
    imagen.pixels[i].a = 255
end

print("Imagen creada: " .. imagen.ancho .. "x" .. imagen.alto)

ffi.C.free(imagen.pixels)

Cuándo usar (y cuándo no usar) LuaJIT

LuaJIT es la elección correcta cuando el rendimiento importa y se puede asumir que la plataforma lo soporta (x86, x86-64, ARM32, ARM64). Sin embargo, tiene limitaciones que hay que tener en cuenta:

  • Implementa Lua 5.1, no 5.4. Algunas bibliotecas modernas requieren 5.4.
  • El JIT no funciona en ciertas plataformas (iOS en modo JIT, algunas configuraciones de seguridad). Hay un modo de solo intérprete (interpreter-only) para esos casos.
  • Las funciones con muchos paths de código distintos pueden no compilarse por JIT (NYI: Not Yet Implemented). La herramienta jit.dump permite ver qué trazas se compilan.
  • La FFI puede introducir bugs difíciles de depurar si se mezcla gestión manual de memoria con el GC de Lua.
-- Comprobar si una función se compila por JIT
local jit = require("jit")
local jutil = require("jit.util")

-- Forzar compilación
jit.on()
local function mi_funcion_critica(n)
    local s = 0
    for i = 1, n do s = s + i end
    return s
end

-- Usar jit.dump para ver trazas (modo debug)
-- require("jit.dump").on("t", "salida.txt")
-- mi_funcion_critica(1000000)

LuaJIT es el punto de convergencia entre el mundo de los lenguajes de scripting y el de los lenguajes de sistemas. Si ya conoces la C API de Lua estándar, la FFI de LuaJIT te resultará mucho más cómoda: en lugar de gestionar la pila de la VM en C, declaras los tipos en C y los llamas directamente desde Lua. Es la misma idea que ctypes en Python, pero integrada con el JIT y con rendimiento muy superior. Para OpenResty, esta combinación —nginx + LuaJIT + FFI— es la base técnica que permite procesar millones de peticiones por segundo con lógica de negocio escrita en Lua.

Imagen: Pexels / Daniil Komov

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP