Quelle est la différence entre LuaJIT et le Lua standard ?
API7.ai
September 23, 2022
Découvrons LuaJIT, un autre pilier d'OpenResty, et je laisserai la partie centrale de ce post à certains aspects essentiels et moins connus de Lua et LuaJIT.
Vous pouvez en apprendre davantage sur les bases de Lua via les moteurs de recherche ou les livres sur Lua, et je recommande le livre Programming in Lua de l'auteur de Lua.
Bien sûr, le seuil pour écrire du code LuaJIT correct dans OpenResty n'est pas élevé. Cependant, il n'est pas facile d'écrire du code LuaJIT efficace, et je couvrirai ici les éléments clés en détail dans la section sur l'optimisation des performances d'OpenResty plus tard.
Voyons où LuaJIT s'insère dans l'architecture globale d'OpenResty.
Comme mentionné précédemment, les processus Worker
d'OpenResty sont obtenus en forkant le processus Master
. La machine virtuelle LuaJIT dans le processus Master
est également forkée. Tous les processus Worker
au sein du même Worker
partagent cette machine virtuelle LuaJIT, et l'exécution du code Lua se fait dans cette machine virtuelle.
Ce sont les bases du fonctionnement d'OpenResty, que nous aborderons plus en détail dans les articles suivants. Aujourd'hui, nous commencerons par clarifier la relation entre Lua et LuaJIT.
Relation entre Lua standard et LuaJIT
Commençons par les éléments essentiels.
Lua standard et LuaJIT sont deux choses différentes. LuaJIT est uniquement compatible avec la syntaxe de Lua 5.1.
La dernière version de Lua standard est actuellement 5.4.4, et la dernière version de LuaJIT est 2.1.0-beta3. Dans les anciennes versions d'OpenResty d'il y a quelques années, vous pouviez choisir d'utiliser soit la machine virtuelle Lua standard, soit la machine virtuelle LuaJIT comme environnement d'exécution lors de la compilation, mais maintenant, le support de Lua standard a été supprimé, et seul LuaJIT est supporté.
La syntaxe de LuaJIT est compatible avec Lua 5.1, avec un support optionnel pour Lua 5.2 et 5.3. Nous devons donc d'abord apprendre la syntaxe de Lua 5.1 et nous appuyer sur celle-ci pour apprendre les fonctionnalités de LuaJIT. Dans l'article précédent, je vous ai présenté la syntaxe de base de Lua. Aujourd'hui, je mentionnerai uniquement certaines fonctionnalités uniques de Lua.
Il est important de noter qu'OpenResty n'utilise pas directement la version officielle de LuaJIT 2.1.0-beta3, mais l'étend avec son fork : openresty-luajit2.
Ces API uniques ont été ajoutées lors du développement réel d'OpenResty pour des raisons de performance. Ainsi, le LuaJIT dont nous parlerons plus tard fait référence à la branche LuaJIT maintenue par OpenResty elle-même.
Pourquoi LuaJIT ?
Après tout ce discours sur la relation entre LuaJIT et Lua, vous vous demandez peut-être pourquoi ne pas utiliser Lua directement, mais utiliser LuaJIT. En fait, la principale raison est l'avantage de performance de LuaJIT.
Le code Lua n'est pas interprété directement, mais compilé en Byte Code
par le compilateur Lua, puis exécuté par la machine virtuelle Lua.
L'environnement d'exécution de LuaJIT, en plus d'une implémentation en assembleur de l'interpréteur Lua, dispose d'un compilateur JIT qui peut générer directement du code machine. Au départ, LuaJIT commence comme Lua standard, avec le code Lua compilé en byte code, qui est interprété et exécuté par l'interpréteur de LuaJIT.
La différence est que l'interpréteur LuaJIT enregistre certaines statistiques d'exécution lors de l'exécution du bytecode, comme le nombre réel de fois où chaque point d'entrée de fonction Lua est exécuté et le nombre réel de fois où chaque boucle Lua est exécutée. Lorsque ces comptes dépassent un seuil aléatoire, le point d'entrée de fonction Lua ou la boucle Lua correspondante est considéré comme suffisamment chaud pour déclencher le compilateur JIT.
Le compilateur JIT tente de compiler le chemin de code Lua correspondant, en commençant par le point d'entrée de la fonction chaude ou l'emplacement de la boucle chaude. Le processus de compilation convertit le bytecode LuaJIT en IR (Intermediate Representation) défini par LuaJIT, puis génère du code machine pour l'architecture cible.
Ainsi, l'optimisation des performances de LuaJIT consiste essentiellement à rendre autant de code Lua que possible disponible pour la génération de code machine par le compilateur JIT, plutôt que de revenir au mode d'exécution interprété de l'interpréteur Lua. Une fois que vous comprenez cela, vous pouvez comprendre la nature de l'optimisation des performances d'OpenResty que vous apprendrez plus tard.
Caractéristiques spéciales de Lua
Comme décrit dans l'article précédent, le langage Lua est relativement simple. Pour les ingénieurs ayant une expérience dans d'autres langages de développement, il est facile de comprendre la logique du code une fois que vous remarquez certains aspects uniques de Lua. Ensuite, examinons certains des aspects les plus inhabituels du langage Lua.
1. L'index commence à 1
Lua est le seul langage de programmation que je connaisse qui commence avec un index de 1
. Cela, bien que mieux compris par les non-programmeurs, est sujet à des bugs de programmation. Voici un exemple.
$ resty -e 't={100}; ngx.say(t[0])'
Vous pourriez vous attendre à ce que le programme imprime 100
ou signale une erreur indiquant que l'index 0
n'existe pas. Mais étonnamment, rien n'est imprimé, et aucune erreur n'est signalée. Ajoutons donc la commande type
et voyons ce que donne la sortie.
$ resty -e 't={100};ngx.say(type(t[0]))'
nil
Il s'avère que c'est la valeur nil
. En fait, dans OpenResty, la détermination et la gestion des valeurs nil
est également un point déroutant, donc nous en reparlerons plus tard lorsque nous parlerons d'OpenResty.
2. Utilisation de ..
pour concaténer des chaînes
Contrairement à la plupart des langages qui utilisent +
, Lua utilise deux points pour concaténer des chaînes.
$ resty -e "ngx.say('hello' .. ', world')"
hello, world
Dans le développement de projets réels, nous utilisons généralement plusieurs langages de développement, et la conception inhabituelle de Lua fera toujours réfléchir les développeurs lorsque la concaténation de chaînes est un peu déroutante.
3. La table est la seule structure de données
Contrairement à Python, un langage riche en structures de données intégrées, Lua n'a qu'une seule structure de données, la table
, qui peut inclure des tableaux et des tables de hachage.
local color = {first = "red", "blue", third = "green", "yellow"}
print(color["first"]) --> output: red
print(color[1]) --> output: blue
print(color["third"]) --> output: green
print(color[2]) --> output: yellow
print(color[3]) --> output: nil
Si vous n'affectez pas explicitement une valeur en tant que paire clé-valeur, la table utilise par défaut un nombre comme index, commençant par 1
. Ainsi, color[1]
est blue
.
De plus, obtenir la longueur correcte dans la table est difficile, alors examinons ces exemples.
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))
Résultat :
Test1 3
Test2 2
Test3 1
Test4
Comme vous pouvez le voir, à l'exception du premier cas de test qui retourne une longueur de 3
, les tests suivants sont tous en dehors de nos attentes. En fait, pour obtenir la longueur de la table en Lua, il est important de noter que la valeur correcte est retournée uniquement si la table est une séquence.
Alors, qu'est-ce qu'une séquence ? Tout d'abord, une séquence est un sous-ensemble d'un tableau. C'est-à-dire que les éléments d'une table sont accessibles avec un index entier positif et qu'il n'y a pas de paires clé-valeur. Dans le code ci-dessus, toutes les tables sont des tableaux sauf t2
.
Ensuite, la séquence ne contient pas de trou, c'est-à-dire nil
. En combinant ces deux points, la table t1
ci-dessus est une séquence, tandis que t3
et t4
sont des tableaux mais pas des séquences.
Jusqu'à présent, vous pouvez encore avoir une question, pourquoi la longueur de t4
sera 1
? C'est parce que lorsque nil
est rencontré, la logique pour obtenir la longueur ne continue pas à s'exécuter mais retourne directement.
Je ne sais pas si vous comprenez complètement. Cette partie est vraiment assez compliquée. Alors, y a-t-il un moyen d'obtenir la longueur de table que nous voulons ? Naturellement, il y en a. OpenResty étend cela, et j'en parlerai plus tard dans le chapitre dédié à la table, alors laissons le suspense ici.
4. Toutes les variables sont globales par défaut
Je tiens à souligner que, sauf si vous en êtes assez certain, vous devriez toujours déclarer de nouvelles variables comme variables local
.
local s = 'hello'
En effet, dans Lua, les variables sont globales par défaut et sont placées dans une table nommée _G
. Les variables qui ne sont pas locales sont recherchées dans la table globale, ce qui est une opération coûteuse. Une faute de frappe dans les noms de variables peut entraîner des bugs difficiles à identifier et à corriger.
Ainsi, dans OpenResty, je recommande fortement de toujours déclarer les variables en utilisant local
, même lorsque vous requirez un module.
-- Recommandé
local xxx = require('xxx')
-- À éviter
require('xxx')
LuaJIT
Avec ces quatre caractéristiques spéciales de Lua à l'esprit, passons à LuaJIT.
LuaJIT, en plus d'être conforme à Lua 5.1 et de supporter JIT, est étroitement intégré avec l'FFI (Foreign Function Interface), vous permettant d'appeler des fonctions C externes et d'utiliser des structures de données C directement dans votre code Lua. Voici l'exemple le plus simple.
local ffi = require("ffi")
ffi.cdef[[
int printf(const char *fmt, ...);
]]
ffi.C.printf("Hello %s!", "world")
En quelques lignes de code seulement, vous pouvez appeler directement la fonction printf
de C depuis Lua et imprimer Hello world!
Vous pouvez utiliser la commande resty
pour l'exécuter et voir si cela fonctionne.
De même, nous pouvons utiliser FFI pour appeler les fonctions C de NGINX et OpenSSL pour faire beaucoup plus. L'approche FFI offre de meilleures performances que l'approche traditionnelle de l'API Lua/C, c'est pourquoi le projet lua-resty-core
existe. Dans la section suivante, nous parlerons de FFI et de lua-resty-core
.
De plus, pour des raisons de performance, LuaJIT étend les fonctions de table : table.new
et table.clear
, deux fonctions essentielles d'optimisation des performances fréquemment utilisées dans la bibliothèque lua-resty
d'OpenResty. Cependant, peu de développeurs les connaissent, car la documentation est dense et il n'y a pas de code d'exemple. Nous les garderons pour la section sur l'optimisation des performances.
Résumé
Passons en revue le contenu d'aujourd'hui.
OpenResty choisit LuaJIT plutôt que Lua standard pour des raisons de performance et maintient sa propre branche LuaJIT. LuaJIT est basé sur la syntaxe de Lua 5.1 et est sélectivement compatible avec certaines syntaxes de Lua 5.2 et Lua 5.3 pour former son système. Quant à la syntaxe Lua que vous devez maîtriser, elle a ses caractéristiques distinctives en matière d'index, de concaténation de chaînes, de structures de données et de variables, auxquelles vous devez prêter une attention particulière lors de l'écriture de code.
Avez-vous rencontré des pièges lors de l'apprentissage de Lua et LuaJIT ? N'hésitez pas à partager vos opinions avec nous, et j'ai écrit un post pour partager les pièges que j'ai rencontrés. Vous êtes également invités à partager ce post avec vos collègues et amis pour apprendre et progresser ensemble.