Les inconvénients du compilateur JIT : pourquoi éviter les fonctionnalités NYI ?
API7.ai
September 30, 2022
Dans l'article précédent, nous avons examiné FFI dans LuaJIT. Si votre projet n'utilise que l'API fournie par OpenResty et que vous n'avez pas besoin d'appeler des fonctions C, alors FFI n'est pas si important pour vous. Vous devez simplement vous assurer que lua-resty-core
est activé.
Mais NYI dans LuaJIT, dont nous allons parler aujourd'hui, est un problème crucial auquel chaque ingénieur utilisant OpenResty ne peut échapper, impactant significativement les performances.
Vous pouvez rapidement écrire du code logiquement correct en utilisant OpenResty, mais sans comprendre NYI, vous ne pouvez pas écrire du code efficace et ne pouvez pas exploiter la puissance d'OpenResty. La différence de performance entre les deux est d'au moins un ordre de grandeur.
Qu'est-ce que NYI ?
Commençons par rappeler un point que nous avons déjà mentionné.
Le runtime 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.
L'implémentation du compilateur JIT dans LuaJIT n'est pas encore complète. Il ne peut pas compiler certaines fonctions car elles sont difficiles à implémenter et parce que les auteurs de LuaJIT sont actuellement semi-retraités. Cela inclut la fonction courante pairs()
, la fonction unpack()
, le module Lua C basé sur l'implémentation Lua CFunction, et ainsi de suite. Cela permet au compilateur JIT de revenir au mode interpréteur lorsqu'il rencontre une opération qu'il ne supporte pas sur le chemin de code actuel.
Le site officiel de LuaJIT a une liste complète de ces NYIs, et je vous suggère de la parcourir. Le but de l'article n'est pas que vous mémorisiez cette liste, mais que vous vous en rappeliez consciemment lorsque vous écrivez du code.
Ci-dessous, j'ai extrait quelques fonctions de la liste NYI pour la bibliothèque de chaînes.
L'état de compilation de string.byte
est oui, ce qui signifie qu'il peut être optimisé avec JIT, et vous pouvez l'utiliser dans votre code sans crainte.
L'état de compilation de string.char
est 2.1, ce qui signifie qu'il est supporté depuis LuaJIT 2.1. Comme nous le savons, LuaJIT dans OpenResty est basé sur LuaJIT 2.1, donc vous pouvez l'utiliser en toute sécurité.
L'état de compilation de string.dump
est never, c'est-à-dire qu'il ne sera pas optimisé avec JIT et reviendra au mode interpréteur. À ce jour, il n'y a pas de plans pour le supporter à l'avenir.
string.find
a un état de compilation de 2.1 partiel, ce qui signifie qu'il est partiellement supporté depuis LuaJIT 2.1, et la note après cela indique qu'il ne supporte que la recherche de chaînes fixes, pas la correspondance de motifs. Donc, pour trouver des chaînes fixes, string.find
peut être optimisé avec JIT.
Naturellement, nous devrions éviter d'utiliser NYI afin que plus de notre code puisse être compilé par JIT et que les performances soient garanties. Cependant, dans un environnement réel, nous avons parfois inévitablement besoin d'utiliser certaines fonctions NYI, alors que devrions-nous faire ?
Alternatives à NYI
Ne vous inquiétez pas. La plupart des fonctions NYI, nous pouvons respectueusement les laisser de côté et implémenter leur fonctionnalité d'autres manières. Ensuite, j'ai sélectionné quelques NYI typiques pour expliquer et vous guider à travers les différents types d'alternatives NYI. De cette façon, vous pouvez également en apprendre davantage sur d'autres NYI.
string.gsub()
Commençons par la fonction string.gsub()
, qui est la fonction de manipulation de chaînes intégrée de Lua qui effectue une substitution globale de chaînes, comme dans l'exemple suivant.
$ resty -e 'local new = string.gsub("banana", "a", "A"); print(new)'
bAnAnA
Cette fonction est une fonction NYI et ne peut pas être compilée par JIT.
Nous pourrions essayer de trouver une fonction de remplacement dans l'API d'OpenResty, mais pour la plupart des gens, il n'est pas pratique de se souvenir de toutes les API et de leur utilisation. C'est pourquoi j'ouvre toujours la page de documentation GitHub pour le lua-nginx-module dans mon travail de développement.
Par exemple, nous pouvons utiliser gsub
comme mot-clé pour rechercher dans la page de documentation, et ngx.re.gsub
viendra à l'esprit.
Nous pouvons également utiliser l'outil restydoc
recommandé précédemment pour rechercher l'API OpenResty. Vous pouvez essayer de l'utiliser pour rechercher gsub
.
$ restydoc -s gsub
Comme vous pouvez le voir, au lieu de retourner le ngx.re.gsub
que nous attendions, les fonctions de Lua sont affichées. En fait, à ce stade, restydoc
retourne une correspondance exacte unique, donc il est plus adapté pour une utilisation si vous connaissez explicitement le nom de l'API. Pour les recherches floues, vous devez toujours le faire manuellement dans la documentation.
Revenant aux résultats de la recherche, nous voyons que la définition de la fonction ngx.re.gsub
est la suivante :
newstr, n, err = ngx.re.gsub(subject, regex, replace, options?)
Ici, les paramètres de la fonction et les valeurs de retour sont nommés avec des significations spécifiques. En fait, dans OpenResty, je ne vous recommande pas d'écrire beaucoup de commentaires. La plupart du temps, un bon nom est meilleur que plusieurs lignes de commentaires.
Pour les ingénieurs peu familiers avec le système de regex d'OpenResty, vous pourriez être confus en voyant la variable options
à la fin. Cependant, l'explication de la variable ne se trouve pas dans cette fonction mais dans la documentation de la fonction ngx.re.match
.
Si vous regardez la documentation pour les options, vous verrez que si nous la définissons à jo
, cela active PCRE JIT
, de sorte que le code utilisant ngx.re.gsub
peut être compilé par JIT par LuaJIT ainsi que par PCRE JIT
.
Je ne vais pas entrer dans les détails de la documentation. La documentation d'OpenResty est excellente, donc lisez-la attentivement et vous pourrez résoudre la plupart de vos problèmes.
string.find()
Contrairement à string.gsub
, string.find
est JIT-able en mode simple (c'est-à-dire la recherche de chaînes), tandis que string.find
n'est pas JIT-able pour les recherches de chaînes avec régularité, ce qui est fait en utilisant l'API d'OpenResty ngx.re.find
.
Donc, lorsque vous faites une recherche de chaîne dans OpenResty, vous devez d'abord clairement distinguer si vous recherchez une chaîne fixe ou une expression régulière. Si c'est le premier cas, utilisez string.find
et n'oubliez pas de définir plain
à true
à la fin.
string.find("foo bar", "foo", 1, true)
Dans le second cas, vous devriez utiliser l'API d'OpenResty et activer l'option JIT pour PCRE.
ngx.re.find("foo bar", "^foo", "jo")
Il serait plus approprié de faire une couche d'encapsulation ici et d'activer les options d'optimisation par défaut, sans laisser l'utilisateur final connaître autant de détails. De cette façon, c'est une fonction de recherche de chaîne uniforme à l'extérieur. Comme vous pouvez le sentir, parfois trop d'options et trop de flexibilité ne sont pas une bonne chose.
unpack()
La troisième fonction que nous allons examiner est unpack()
. unpack()
est également une fonction qui doit être évitée, surtout pas dans le corps de la boucle. Au lieu de cela, vous pouvez y accéder en utilisant les numéros d'index d'un tableau, comme dans cet exemple du code suivant.
$ resty -e '
local a = {100, 200, 300, 400}
for i = 1, 2 do
print(unpack(a))
end'
$ resty -e 'local a = {100, 200, 300, 400}
for i = 1, 2 do
print(a[1], a[2], a[3], a[4])
end'
Creusons un peu plus profondément dans unpack
, et cette fois nous pouvons utiliser restydoc
pour rechercher.
$ restydoc -s unpack
Comme vous pouvez le voir dans la documentation de unpack, unpack(list [, i [, j]])
est équivalent à return list[i], list[i+1], list[j]
, et vous pouvez considérer unpack comme du sucre syntaxique. De cette façon, vous pouvez y accéder exactement comme un index de tableau sans casser la compilation JIT de LuaJIT.
pairs()
Enfin, regardons la fonction pairs()
qui parcourt la table de hachage, qui ne peut pas non plus être compilée par JIT.
Malheureusement, cependant, il n'y a pas d'alternative équivalente à cela. Vous ne pouvez qu'essayer de l'éviter ou utiliser des tableaux accessibles par index numérique à la place, et en particulier, ne pas parcourir la table de hachage sur le chemin de code chaud. Ici, j'explique le chemin de code chaud, ce qui signifie que le code sera exécuté de nombreuses fois, par exemple, à l'intérieur d'une boucle géante.
Après avoir dit ces quatre exemples, résumons que pour contourner l'utilisation des fonctions NYI, vous devez faire attention à ces deux points.
- Utilisez l'API fournie par OpenResty de préférence aux fonctions de la bibliothèque standard de Lua. Rappelez-vous que Lua est un langage embarqué, et nous programmons dans OpenResty, pas dans Lua.
- Si vous devez utiliser le langage NYI en dernier recours, assurez-vous qu'il n'est pas sur le chemin de code chaud.
Comment détecter NYI ?
Tout ce discours sur le contournement de NYI est pour vous apprendre quoi faire. Cependant, cela serait en contradiction avec l'une des philosophies qu'OpenResty défend si cela se terminait abruptement ici.
Ce qui peut être fait automatiquement par la machine n'implique pas les humains.
Les gens ne sont pas des machines, et il y aura toujours des oublis. Automatiser la détection des NYI utilisés dans le code est une réflexion essentielle de la valeur d'un ingénieur.
Ici, je recommande les modules jit.dump
et jit.v
qui viennent avec LuaJIT. Ils impriment tous deux le processus de fonctionnement du compilateur JIT. Le premier produit des informations détaillées qui peuvent être utilisées pour déboguer LuaJIT lui-même. Vous pouvez vous référer à son code source pour une compréhension plus approfondie ; le second produit une sortie plus simple, chaque ligne correspondant à une trace, et est généralement utilisé pour vérifier si elle peut être JIT.
Comment devrions-nous faire cela ? Nous pouvons commencer par ajouter les deux lignes de code suivantes à init_by_lua
.
local v = require "jit.v"
v.on("/tmp/jit.log")
Ensuite, exécutez votre outil de test de charge ou quelques centaines de jeux de tests unitaires pour que LuaJIT soit suffisamment chaud pour déclencher la compilation JIT. Une fois cela fait, vérifiez les résultats de /tmp/jit.log
.
Bien sûr, cette approche est relativement fastidieuse, donc si vous voulez garder les choses simples, resty
est suffisant, et l'interface de ligne de commande d'OpenResty vient avec les options suivantes.
$resty -j v -e 'for i=1, 1000 do
local newstr, n, err = ngx.re.gsub("hello, world", "([a-z])[a-z]+", "[$0,$1]", "i")
end'
[TRACE 1 (command line -e):1 stitch C:107bc91fd]
[TRACE 2 (1/stitch) (command line -e):2 -> 1]
Où -j
dans resty
est l'option liée à LuaJIT, les valeurs dump et v suivent, correspondant à l'activation des modes jit.dump
et jit.v
.
Dans la sortie du module jit.v
, chaque ligne est un objet trace compilé avec succès. Juste maintenant est un exemple de trace capable de JIT, et si des fonctions NYI sont rencontrées, la sortie spécifiera qu'elles sont NYI, comme dans l'exemple suivant de pairs
.
$resty -j v -e 'local t = {}
for i=1,100 do
t[i] = i
end
for i=1, 1000 do
for j=1,1000 do
for k,v in pairs(t) do
--
end
end
end'
Il ne peut pas être JIT'd, donc le résultat indique une fonction NYI à la ligne 8.
[TRACE 1 (command line -e):2 loop]
[TRACE --- (command line -e):7 -- NYI: bytecode 72 at (command line -e):8]
Écrit à la fin
C'est la première fois que nous parlons des problèmes de performance d'OpenResty de manière plus approfondie. Après avoir lu ces optimisations sur NYI, qu'en pensez-vous ? Vous pouvez laisser un commentaire avec votre opinion.
Enfin, je vous laisse avec une question stimulante lors de la discussion sur les alternatives à la fonction string.find()
; j'ai mentionné qu'il serait préférable de faire une couche d'encapsulation et d'activer les options d'optimisation par défaut. Donc, je vous laisse cette tâche pour un petit essai.
N'hésitez pas à écrire vos réponses dans la section des commentaires, et vous êtes invités à partager cet article avec vos collègues et amis pour communiquer et progresser ensemble.