¿Cuál es la diferencia entre LuaJIT y Lua estándar?
API7.ai
September 23, 2022
Aprendamos sobre LuaJIT, otro pilar fundamental de OpenResty, y dejaré la parte central de la publicación de hoy para algunos aspectos esenciales y menos conocidos de Lua y LuaJIT.
Puedes aprender más sobre los conceptos básicos de Lua a través de motores de búsqueda o libros de Lua, y recomiendo el libro Programming in Lua del autor de Lua.
Por supuesto, el umbral para escribir código LuaJIT correcto en OpenResty no es alto. Sin embargo, no es fácil escribir código LuaJIT eficiente, y cubriré los elementos clave aquí en detalle en la sección de optimización de rendimiento de OpenResty más adelante.
Veamos dónde encaja LuaJIT en la arquitectura general de OpenResty.
Como se mencionó anteriormente, los procesos Worker
de OpenResty se obtienen bifurcando el proceso Master
. La máquina virtual LuaJIT en el proceso Master
también se bifurca. Todos los procesos Worker
dentro del mismo Worker
comparten esta máquina virtual LuaJIT, y la ejecución del código Lua se realiza en esta máquina virtual.
Estos son los conceptos básicos de cómo funciona OpenResty, que discutiremos con más detalle en artículos posteriores. Hoy comenzaremos aclarando la relación entre Lua y LuaJIT.
Relación entre Lua estándar y LuaJIT
Primero, aclaremos lo esencial.
Lua estándar y LuaJIT son dos cosas diferentes. LuaJIT solo es compatible con la sintaxis de Lua 5.1.
La última versión de Lua estándar es ahora 5.4.4, y la última versión de LuaJIT es 2.1.0-beta3. En versiones antiguas de OpenResty de hace algunos años, podías elegir usar la máquina virtual Lua estándar o la máquina virtual LuaJIT como entorno de ejecución al compilar, pero ahora se ha eliminado el soporte para Lua estándar, y solo se admite LuaJIT.
La sintaxis de LuaJIT es compatible con Lua 5.1, con soporte opcional para Lua 5.2 y 5.3. Por lo tanto, primero debemos aprender la sintaxis de Lua 5.1 y luego construir sobre eso para aprender las características de LuaJIT. En el artículo anterior, te llevé a través de la sintaxis básica de Lua. Hoy solo mencionaré algunas características únicas de Lua.
Vale la pena señalar que OpenResty no utiliza directamente la versión oficial de LuaJIT 2.1.0-beta3, sino que la extiende con su bifurcación: openresty-luajit2.
Estas API únicas se agregaron durante el desarrollo real de OpenResty por razones de rendimiento. Por lo tanto, el LuaJIT que mencionamos más adelante se refiere a la rama de LuaJIT mantenida por OpenResty.
¿Por qué LuaJIT?
Después de todo esto sobre la relación entre LuaJIT y Lua, quizás te preguntes por qué no usar Lua directamente, sino LuaJIT. De hecho, la razón principal es la ventaja de rendimiento de LuaJIT.
El código Lua no se interpreta directamente, sino que se compila en Byte Code
por el compilador de Lua y luego se ejecuta en la máquina virtual de Lua.
El entorno de ejecución de LuaJIT, además de una implementación en ensamblador del intérprete de Lua, tiene un compilador JIT que puede generar código de máquina directamente. Al principio, LuaJIT comienza como Lua estándar, con el código Lua compilado en bytecode, que es interpretado y ejecutado por el intérprete de LuaJIT.
La diferencia es que el intérprete de LuaJIT registra algunas estadísticas de tiempo de ejecución mientras ejecuta el bytecode, como el número real de veces que se ejecuta cada entrada de función Lua y el número real de veces que se ejecuta cada bucle Lua. Cuando estos conteos superan un umbral aleatorio, se considera que la entrada de función Lua o el bucle Lua están lo suficientemente calientes como para activar el compilador JIT.
El compilador JIT intenta compilar la ruta de código Lua correspondiente, comenzando desde la entrada de la función caliente o la ubicación del bucle caliente. El proceso de compilación convierte el bytecode de LuaJIT en IR (Representación Intermedia) definida por LuaJIT y luego genera código de máquina para la arquitectura objetivo.
Por lo tanto, la optimización de rendimiento de LuaJIT se trata esencialmente de hacer que la mayor cantidad de código Lua posible esté disponible para la generación de código de máquina por el compilador JIT, en lugar de caer en el modo de ejecución interpretada del intérprete de Lua. Una vez que entiendas esto, podrás comprender la naturaleza de la optimización de rendimiento de OpenResty que aprenderás más adelante.
Características especiales de Lua
Como se describió en el artículo anterior, el lenguaje Lua es relativamente simple. Para ingenieros con experiencia en otros lenguajes de desarrollo, es fácil ver la lógica del código una vez que notas algunos aspectos únicos de Lua. A continuación, veamos algunos de los aspectos más inusuales del lenguaje Lua.
1. El índice comienza desde 1
Lua es el único lenguaje de programación que conozco que comienza con un índice de 1
. Esto, aunque es más fácil de entender para personas sin experiencia en programación, es propenso a errores de programación. Aquí hay un ejemplo.
$ resty -e 't={100}; ngx.say(t[0])'
Podrías esperar que el programa imprima 100
o que informe un error diciendo que el índice 0
no existe. Pero sorprendentemente, no se imprime nada y no se reportan errores. Así que agreguemos el comando type
y veamos cuál es la salida.
$ resty -e 't={100};ngx.say(type(t[0]))'
nil
Resulta ser el valor nil
. De hecho, en OpenResty, la determinación y el manejo de valores nil
también es un punto confuso, por lo que hablaremos más sobre esto más adelante cuando hablemos de OpenResty.
2. Usar ..
para concatenar cadenas
A diferencia de la mayoría de los lenguajes que usan +
, Lua usa dos puntos para concatenar cadenas.
$ resty -e "ngx.say('hello' .. ', world')"
hello, world
En el desarrollo de proyectos reales, generalmente usamos múltiples lenguajes de desarrollo, y el diseño inusual de Lua siempre hará que los desarrolladores piensen cuando la concatenación de cadenas se vuelve un poco confusa.
3. La tabla es la única estructura de datos
A diferencia de Python, un lenguaje rico en estructuras de datos integradas, Lua tiene solo una estructura de datos, la table
, que puede incluir arreglos y tablas hash.
local color = {first = "red", "blue", third = "green", "yellow"}
print(color["first"]) --> salida: red
print(color[1]) --> salida: blue
print(color["third"]) --> salida: green
print(color[2]) --> salida: yellow
print(color[3]) --> salida: nil
Si no asignas explícitamente un valor como un par clave-valor, la tabla usa un número como índice por defecto, comenzando desde 1
. Por lo tanto, color[1]
es blue
.
Además, obtener la longitud correcta en la tabla es difícil, así que veamos estos ejemplos.
local t1 = { 1, 2, 3 }
print("Test1 " .. table.getn(t1))
local t2 = { 1, a = 2, 3 }
print("Test2 " .. table.getn(t2))
local t3 = { 1, nil }
print("Test3 " .. table.getn(t3))
local t4 = { 1, nil, 2 }
print("Test4 " .. table.getn(t4))
Resultado:
Test1 3
Test2 2
Test3 1
Test4
Como puedes ver, excepto por el primer caso de prueba que devuelve una longitud de 3
, las pruebas posteriores están fuera de nuestras expectativas. De hecho, para obtener la longitud de la tabla en Lua, es importante tener en cuenta que solo se devuelve el valor correcto si la tabla es una secuencia.
Entonces, ¿qué es una secuencia? En primer lugar, una secuencia es un subconjunto de un arreglo. Es decir, los elementos de una tabla son accesibles con un índice entero positivo y no hay pares clave-valor. En el código anterior, todas las tablas son arreglos excepto t2
.
En segundo lugar, la secuencia no contiene un agujero, es decir, nil
. Combinando estos dos puntos, la tabla t1
es una secuencia, mientras que t3
y t4
son arreglos pero no secuencias.
Hasta este punto, quizás aún tengas una pregunta, ¿por qué la longitud de t4
será 1
? Esto se debe a que cuando se encuentra nil
, la lógica para obtener la longitud no continúa ejecutándose, sino que regresa directamente.
No sé si lo entiendes completamente. Esta parte es realmente bastante complicada. Entonces, ¿hay alguna manera de obtener la longitud de la tabla que queremos? Naturalmente, la hay. OpenResty extiende esto, y hablaré de ello más adelante en el capítulo dedicado a la tabla, así que dejemos el suspenso aquí.
4. Todas las variables son globales por defecto
Me gustaría enfatizar que, a menos que estés bastante seguro, siempre debes declarar nuevas variables como variables local
.
local s = 'hello'
Esto se debe a que, en Lua, las variables son globales por defecto y se colocan en una tabla llamada _G
. Las variables que no son locales se buscan en la tabla global, lo cual es una operación costosa. Los errores de ortografía en los nombres de las variables pueden llevar a errores difíciles de identificar y corregir.
Por lo tanto, en OpenResty, recomiendo encarecidamente que siempre declares variables usando local
, incluso cuando requieras un módulo.
-- Recomendado
local xxx = require('xxx')
-- Evitar
require('xxx')
LuaJIT
Con estas cuatro características especiales de Lua en mente, pasemos a LuaJIT.
LuaJIT, además de ser compatible con Lua 5.1 y admitir JIT, está estrechamente integrado con FFI (Interfaz de Funciones Extranjeras), lo que te permite llamar a funciones C externas y usar estructuras de datos C directamente en tu código Lua. Aquí está el ejemplo más simple.
local ffi = require("ffi")
ffi.cdef[[
int printf(const char *fmt, ...);
]]
ffi.C.printf("Hello %s!", "world")
En solo unas pocas líneas de código, puedes llamar directamente a la función printf
de C desde Lua e imprimir Hello world!
Puedes usar el comando resty
para ejecutarlo y ver si funciona.
De manera similar, podemos usar FFI para llamar a las funciones C de NGINX y OpenSSL para hacer mucho más. El enfoque FFI tiene un mejor rendimiento que el enfoque tradicional de la API Lua/C, razón por la cual existe el proyecto lua-resty-core
. En la siguiente sección, hablaremos sobre FFI y lua-resty-core
.
Además, por razones de rendimiento, LuaJIT extiende las funciones de la tabla: table.new
y table.clear
, dos funciones esenciales de optimización de rendimiento que se usan con frecuencia en la biblioteca lua-resty
de OpenResty. Sin embargo, pocos desarrolladores están familiarizados con ellas, ya que la documentación es intensa y no hay código de ejemplo. Las dejaremos para la sección de optimización de rendimiento.
Resumen
Repasemos el contenido de hoy.
OpenResty elige LuaJIT en lugar de Lua estándar por razones de rendimiento y mantiene su propia rama de LuaJIT. LuaJIT se basa en la sintaxis de Lua 5.1 y es selectivamente compatible con algunas sintaxis de Lua 5.2 y Lua 5.3 para formar su sistema. En cuanto a la sintaxis de Lua que necesitas dominar, tiene características distintivas en el índice, la concatenación de cadenas, las estructuras de datos y las variables, a las que debes prestar especial atención al escribir código.
¿Has encontrado algún obstáculo al aprender Lua y LuaJIT? No dudes en compartir tus opiniones con nosotros, y he escrito una publicación para compartir los obstáculos que he encontrado. También te invito a compartir esta publicación con tus colegas y amigos para aprender y progresar juntos.