A Desvantagem do Compilador JIT: Por Que Evitar NYI?
API7.ai
September 30, 2022
No artigo anterior, examinamos o FFI no LuaJIT. Se o seu projeto usa apenas a API fornecida pelo OpenResty e você não precisa chamar funções C, então o FFI não é tão importante para você. Você só precisa garantir que o lua-resty-core
esteja habilitado.
Mas o NYI no LuaJIT, que discutiremos hoje, é um problema crucial que todo engenheiro que usa o OpenResty não pode evitar, impactando significativamente o desempenho.
Você pode escrever rapidamente um código logicamente correto usando o OpenResty, mas sem entender o NYI, você não pode escrever um código eficiente e não pode aproveitar o poder do OpenResty. A diferença de desempenho entre os dois é de pelo menos uma ordem de magnitude.
O que é NYI?
Vamos começar relembrando um ponto que já mencionamos antes.
O runtime do LuaJIT, além de uma implementação em assembly do interpretador Lua, possui um compilador JIT que pode gerar código de máquina diretamente.
A implementação do compilador JIT no LuaJIT ainda não está completa. Ele não pode compilar algumas funções porque são difíceis de implementar e porque os autores do LuaJIT estão atualmente semi-aposentados. Isso inclui funções comuns como pairs()
, unpack()
, módulos Lua baseados na implementação Lua CFunction, e assim por diante. Isso permite que o compilador JIT volte ao modo interpretador quando encontra uma operação que não suporta no caminho de código atual.
O site oficial do LuaJIT tem uma lista completa desses NYIs, e eu sugiro que você a revise. O objetivo do artigo não é que você memorize essa lista, mas que você se lembre conscientemente dela ao escrever código.
Abaixo, selecionei algumas funções da lista NYI para a biblioteca de strings.
O status de compilação de string.byte
é "sim", o que significa que ele pode ser otimizado com JIT, e você pode usá-lo em seu código sem medo.
O status de compilação de string.char
é 2.1, o que significa que ele é suportado desde o LuaJIT 2.1. Como sabemos, o LuaJIT no OpenResty é baseado no LuaJIT 2.1, então você pode usá-lo com segurança.
O estado de compilação de string.dump
é "nunca", ou seja, ele não será otimizado com JIT e voltará ao modo interpretador. Até o momento, não há planos para suportar isso no futuro.
string.find
tem um status de compilação de 2.1 parcial, o que significa que ele é parcialmente suportado desde o LuaJIT 2.1, e a nota após isso diz que ele só suporta a busca por strings fixas, não correspondência de padrões. Portanto, para encontrar strings fixas, string.find
pode ser otimizado com JIT.
Naturalmente, devemos evitar usar NYI para que mais do nosso código possa ser compilado com JIT e o desempenho seja garantido. No entanto, em um ambiente real, às vezes inevitavelmente precisamos usar algumas funções NYI, então o que fazer?
Alternativas ao NYI
Não se preocupe. A maioria das funções NYI podemos respeitosamente deixar para trás e implementar sua funcionalidade de outras maneiras. A seguir, selecionei alguns NYIs típicos para explicar e guiá-lo pelos diferentes tipos de alternativas ao NYI. Dessa forma, você também pode aprender sobre outros NYIs.
string.gsub()
Vamos primeiro olhar para a função string.gsub()
, que é uma função embutida do Lua para manipulação de strings que faz substituição global de strings, como no exemplo a seguir.
$ resty -e 'local new = string.gsub("banana", "a", "A"); print(new)'
bAnAnA
Esta função é uma função NYI e não pode ser compilada pelo JIT.
Poderíamos tentar encontrar uma função substituta na API do OpenResty, mas para a maioria das pessoas, não é prático lembrar de todas as APIs e seus usos. É por isso que eu sempre abro a página de documentação do lua-nginx-module no GitHub durante meu trabalho de desenvolvimento.
Por exemplo, podemos usar gsub
como uma palavra-chave para pesquisar na página de documentação, e ngx.re.gsub
virá à mente.
Também podemos usar a ferramenta restydoc
recomendada anteriormente para pesquisar a API do OpenResty. Você pode tentar usá-la para pesquisar por gsub
.
$ restydoc -s gsub
Como você pode ver, em vez de retornar o ngx.re.gsub
que esperávamos, as funções do Lua são mostradas. Na verdade, nesta fase, o restydoc
retorna uma correspondência exata única, então é mais adequado para uso se você conhecer explicitamente o nome da API. Para pesquisas difusas, você ainda precisa fazer isso manualmente na documentação.
Voltando aos resultados da pesquisa, vemos que a definição da função ngx.re.gsub
é a seguinte:
newstr, n, err = ngx.re.gsub(subject, regex, replace, options?)
Aqui, os parâmetros da função e os valores de retorno são nomeados com significados específicos. Na verdade, no OpenResty, eu não recomendo que você escreva muitos comentários. Na maioria das vezes, um bom nome é melhor do que várias linhas de comentários.
Para engenheiros não familiarizados com o sistema de expressões regulares do OpenResty, você pode ficar confuso ao ver a variável options
no final. No entanto, a explicação da variável não está nesta função, mas na documentação da função ngx.re.match
.
Se você olhar a documentação para options
, verá que se a configurarmos como jo
, ela ativa o PCRE JIT
, de modo que o código usando ngx.re.gsub
pode ser compilado pelo JIT do LuaJIT e também pelo PCRE JIT
.
Não vou entrar em detalhes sobre a documentação. A documentação do OpenResty é excelente, então leia-a com atenção e você poderá resolver a maioria dos seus problemas.
string.find()
Diferente de string.gsub
, string.find
é JIT-able no modo simples (ou seja, busca de string), enquanto string.find
não é JIT-able para buscas de string com regularidade, o que é feito usando a API do OpenResty ngx.re.find
.
Portanto, quando você faz uma busca de string no OpenResty, deve primeiro distinguir claramente se está procurando uma string fixa ou uma expressão regular. Se for o primeiro caso, use string.find
e lembre-se de definir plain
como true
no final.
string.find("foo bar", "foo", 1, true)
No segundo caso, você deve usar a API do OpenResty e ativar a opção JIT para PCRE.
ngx.re.find("foo bar", "^foo", "jo")
Seria mais apropriado fazer uma camada de encapsulamento aqui e ativar as opções de otimização por padrão, sem deixar que o usuário final saiba tantos detalhes. Dessa forma, é uma função de busca de string uniforme para o exterior. Como você pode sentir, às vezes muitas opções e muita flexibilidade não são uma coisa boa.
unpack()
A terceira função que veremos é unpack()
. unpack()
também é uma função que precisa ser evitada, especialmente não no corpo do loop. Em vez disso, você pode acessá-la usando os números de índice de um array, como no exemplo do código a seguir.
$ 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'
Vamos nos aprofundar um pouco mais em unpack
, e desta vez podemos usar restydoc
para pesquisar.
$ restydoc -s unpack
Como você pode ver na documentação de unpack
, unpack(list [, i [, j]])
é equivalente a return list[i], list[i+1], list[j]
, e você pode pensar em unpack
como açúcar sintático. Dessa forma, você pode acessá-lo exatamente como um índice de array sem quebrar a compilação JIT do LuaJIT.
pairs()
Finalmente, vamos olhar para a função pairs()
que percorre a tabela hash, que também não pode ser compilada pelo JIT.
No entanto, infelizmente, não há uma alternativa equivalente para isso. Você só pode tentar evitá-la ou usar arrays acessados por índice numérico, e especialmente, não percorrer a tabela hash no caminho de código quente. Aqui eu explico o caminho de código quente, que significa que o código será executado muitas vezes, por exemplo, dentro de um loop gigante.
Tendo dito esses quatro exemplos, vamos resumir que para contornar o uso de funções NYI, você precisa prestar atenção a esses dois pontos.
- Use a API fornecida pelo OpenResty em preferência às funções da biblioteca padrão do Lua. Lembre-se de que o Lua é uma linguagem embutida, e estamos programando no OpenResty, não no Lua.
- Se você tiver que usar o NYI como último recurso, certifique-se de que não está no caminho de código quente.
Como detectar NYI?
Toda essa conversa sobre contornar o NYI é para ensinar o que fazer. No entanto, seria inconsistente com uma das filosofias que o OpenResty defende se terminasse abruptamente aqui.
O que pode ser feito automaticamente pela máquina não envolve humanos.
As pessoas não são máquinas, e sempre haverá oversights. Automatizar a detecção de NYI usado no código é uma reflexão essencial do valor de um engenheiro.
Aqui eu recomendo os módulos jit.dump
e jit.v
que vêm com o LuaJIT. Ambos imprimem o processo de como o compilador JIT funciona. O primeiro produz informações detalhadas que podem ser usadas para depurar o próprio LuaJIT. Você pode consultar seu código-fonte para um entendimento mais profundo; o segundo produz uma saída mais direta, com cada linha correspondendo a um trace, e é geralmente usado para verificar se ele pode ser JIT.
Como devemos fazer isso? Podemos começar adicionando as seguintes duas linhas de código ao init_by_lua
.
local v = require "jit.v"
v.on("/tmp/jit.log")
Em seguida, execute sua ferramenta de teste de estresse ou algumas centenas de conjuntos de testes unitários para aquecer o LuaJIT o suficiente para acionar a compilação JIT. Uma vez feito isso, verifique os resultados de /tmp/jit.log
.
Claro, essa abordagem é relativamente tediosa, então se você quiser manter as coisas simples, resty
é suficiente, e o CLI do OpenResty vem com as seguintes opções.
$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]
Onde -j
em resty
é a opção relacionada ao LuaJIT, os valores dump e v seguem, correspondendo a ativar o modo jit.dump
e jit.v
.
Na saída do módulo jit.v
, cada linha é um objeto trace compilado com sucesso. Agora mesmo é um exemplo de um trace JIT-capable, e se funções NYI forem encontradas, a saída especificará que elas são NYIs, como no exemplo do seguinte 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'
Ele não pode ser JIT'd, então o resultado indica uma função NYI na linha 8.
[TRACE 1 (command line -e):2 loop]
[TRACE --- (command line -e):7 -- NYI: bytecode 72 at (command line -e):8]
Escrito no final
Esta é a primeira vez que falamos sobre problemas de desempenho do OpenResty com mais detalhes. Depois de ler essas otimizações sobre NYI, o que você acha? Você pode deixar um comentário com sua opinião.
Finalmente, vou deixar uma questão instigante ao discutir alternativas à função string.find()
; eu mencionei que seria melhor fazer uma camada de encapsulamento e ativar as opções de otimização por padrão. Então, vou deixar essa tarefa para você como um pequeno teste.
Sinta-se à vontade para escrever suas respostas na seção de comentários, e você é bem-vindo para compartilhar este artigo com seus colegas e amigos para se comunicar e progredir juntos.