Недостаток JIT-компилятора: почему следует избегать NYI?

API7.ai

September 30, 2022

OpenResty (NGINX + Lua)

В предыдущей статье мы рассмотрели FFI в LuaJIT. Если ваш проект использует только API, предоставляемое OpenResty, и вам не нужно вызывать функции на C, то FFI не так важен для вас. Вам просто нужно убедиться, что lua-resty-core включен.

Но NYI в LuaJIT, о котором мы поговорим сегодня, — это важная проблема, с которой сталкивается каждый инженер, использующий OpenResty, и она значительно влияет на производительность.

Вы можете быстро написать логически правильный код с использованием OpenResty, но без понимания NYI вы не сможете написать эффективный код и не сможете использовать всю мощь OpenResty. Разница в производительности между ними составляет как минимум порядок величины.

Что такое NYI?

Давайте начнем с того, что вспомним один момент, о котором мы уже говорили ранее.

Среда выполнения LuaJIT, помимо реализации интерпретатора Lua на ассемблере, имеет JIT-компилятор, который может генерировать машинный код напрямую.

Реализация JIT-компилятора в LuaJIT еще не завершена. Он не может компилировать некоторые функции, потому что их сложно реализовать, а также потому, что авторы LuaJIT в настоящее время находятся в полу-пенсионном состоянии. К ним относятся такие функции, как pairs(), unpack(), Lua C-модули, основанные на реализации Lua CFunction, и так далее. Это позволяет JIT-компилятору переключаться в режим интерпретатора, когда он сталкивается с операцией, которую не поддерживает на текущем пути выполнения.

На официальном сайте LuaJIT есть полный список этих NYI, и я рекомендую вам ознакомиться с ним. Цель статьи не в том, чтобы вы запомнили этот список, а в том, чтобы вы сознательно напоминали себе о нем при написании кода.

Ниже я привел несколько функций из списка NYI для библиотеки строк.

string library

Состояние компиляции string.byte — "да", что означает, что она может быть оптимизирована с помощью JIT, и вы можете использовать ее в своем коде без опасений.

Состояние компиляции string.char — 2.1, что означает, что она поддерживается начиная с LuaJIT 2.1. Как мы знаем, LuaJIT в OpenResty основан на LuaJIT 2.1, поэтому вы можете использовать ее безопасно.

Состояние компиляции string.dump — "никогда", то есть она не будет оптимизирована с помощью JIT и переключится в режим интерпретатора. На данный момент нет планов по поддержке этой функции в будущем.

string.find имеет состояние компиляции 2.1 partial, что означает, что она частично поддерживается начиная с LuaJIT 2.1, и примечание после этого говорит, что она поддерживает только поиск фиксированных строк, а не сопоставление с шаблоном. Таким образом, для поиска фиксированных строк string.find может быть оптимизирована с помощью JIT.

Естественно, мы должны избегать использования NYI, чтобы больше нашего кода могло быть скомпилировано JIT, и производительность могла быть гарантирована. Однако в реальной среде мы иногда неизбежно должны использовать некоторые функции NYI, так что же делать?

Альтернативы NYI

Не беспокойтесь. Большинство функций NYI мы можем с уважением оставить в прошлом и реализовать их функциональность другими способами. Далее я выбрал несколько типичных NYI, чтобы объяснить и провести вас через различные типы альтернатив NYI. Таким образом, вы также сможете узнать о других NYI.

string.gsub()

Давайте сначала рассмотрим функцию string.gsub(), которая является встроенной функцией Lua для работы со строками, выполняющей глобальную замену строк, например, как в следующем примере.

$ resty -e 'local new = string.gsub("banana", "a", "A"); print(new)' bAnAnA

Эта функция является NYI-функцией и не может быть скомпилирована JIT.

Мы могли бы попытаться найти заменяющую функцию в API OpenResty, но для большинства людей запомнить все API и их использование нереально. Именно поэтому я всегда открываю страницу документации на GitHub для lua-nginx-module в своей разработке.

Например, мы можем использовать gsub в качестве ключевого слова для поиска на странице документации, и ngx.re.gsub придет на ум.

Мы также можем использовать инструмент restydoc, рекомендованный ранее, для поиска API OpenResty. Вы можете попробовать использовать его для поиска gsub.

$ restydoc -s gsub

Как видите, вместо возвращения ожидаемого ngx.re.gsub показываются функции Lua. На самом деле, на этом этапе restydoc возвращает точное уникальное совпадение, поэтому он больше подходит для использования, если вы точно знаете имя API. Для нечеткого поиска вам все равно придется делать это вручную в документации.

Возвращаясь к результатам поиска, мы видим, что определение функции ngx.re.gsub выглядит следующим образом:

newstr, n, err = ngx.re.gsub(subject, regex, replace, options?)

Здесь параметры функции и возвращаемые значения названы с конкретными значениями. На самом деле, в OpenResty я не рекомендую вам писать много комментариев. В большинстве случаев хорошее имя лучше, чем несколько строк комментариев.

Для инженеров, не знакомых с системой регулярных выражений OpenResty, вы можете быть смущены, когда увидите переменную options в конце. Однако объяснение переменной находится не в этой функции, а в документации для функции ngx.re.match.

Если вы посмотрите документацию по options, вы увидите, что если мы установим ее в jo, то включится PCRE JIT, так что код, использующий ngx.re.gsub, может быть скомпилирован JIT как LuaJIT, так и PCRE JIT.

Я не буду углубляться в детали документации. Документация OpenResty отличная, поэтому внимательно прочитайте ее, и вы сможете решить большинство своих проблем.

string.find()

В отличие от string.gsub, string.find может быть JIT-компилируемой в простом режиме (то есть поиск строки), в то время как string.find не может быть JIT-компилируемой для поиска строк с регулярными выражениями, что делается с использованием API OpenResty ngx.re.find.

Таким образом, когда вы выполняете поиск строки в OpenResty, вы должны сначала четко определить, ищете ли вы фиксированную строку или регулярное выражение. Если это первое, используйте string.find и не забудьте установить plain в true в конце.

string.find("foo bar", "foo", 1, true)

В последнем случае вы должны использовать API OpenResty и включить опцию JIT для PCRE.

ngx.re.find("foo bar", "^foo", "jo")

Здесь было бы более уместно сделать слой обертки и включить опции оптимизации по умолчанию, не позволяя конечному пользователю знать так много деталей. Таким образом, это будет единая функция поиска строки для внешнего мира. Как вы можете почувствовать, иногда слишком много опций и слишком много гибкости — это не всегда хорошо.

unpack()

Третья функция, которую мы рассмотрим, — это unpack(). unpack() также является функцией, которую нужно избегать, особенно не в теле цикла. Вместо этого вы можете получить доступ к ней с использованием индексов массива, как в этом примере из следующего кода.

$ 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'

Давайте углубимся немного в unpack, и на этот раз мы можем использовать restydoc для поиска.

$ restydoc -s unpack

Как видно из документации по unpack, unpack(list [, i [, j]]) эквивалентно return list[i], list[i+1], list[j], и вы можете думать о unpack как о синтаксическом сахаре. Таким образом, вы можете получить доступ к ней точно так же, как к индексу массива, не нарушая JIT-компиляцию LuaJIT.

pairs()

Наконец, давайте рассмотрим функцию pairs(), которая обходит хэш-таблицу, и она также не может быть скомпилирована JIT.

К сожалению, однако, нет эквивалентной альтернативы для этого. Вы можете только попытаться избежать ее или использовать массивы, доступные по числовому индексу, и, в частности, не обходить хэш-таблицу на горячем пути кода. Здесь я объясняю горячий путь кода, что означает, что код будет возвращаться для выполнения много раз, например, внутри гигантского цикла.

Сказав эти четыре примера, давайте подведем итог, что для обхода использования функций NYI вам нужно обратить внимание на эти два момента.

  • Используйте API, предоставляемое OpenResty, предпочтительно перед функциями стандартной библиотеки Lua. Помните, что Lua — это встраиваемый язык, и мы программируем в OpenResty, а не в Lua.
  • Если вам приходится использовать NYI в крайнем случае, убедитесь, что это не на горячем пути кода.

Как обнаружить NYI?

Все эти разговоры об обходе NYI — это чтобы научить вас, что делать. Однако, если бы это закончилось здесь, это было бы несовместимо с одной из философий, которые пропагандирует OpenResty.

Что может быть сделано автоматически машиной, не должно вовлекать человека.

Люди — не машины, и всегда будут упущения. Автоматизация обнаружения NYI, используемого в коде, — это важное отражение ценности инженера.

Здесь я рекомендую модули jit.dump и jit.v, которые поставляются с LuaJIT. Они оба выводят процесс работы JIT-компилятора. Первый выводит подробную информацию, которая может быть использована для отладки самого LuaJIT. Вы можете обратиться к его исходному коду для более глубокого понимания; второй вывод более простой, каждая строка соответствует трассировке, и обычно используется для проверки, может ли она быть JIT.

Как мы должны это делать? Мы можем начать с добавления следующих двух строк кода в init_by_lua.

local v = require "jit.v" v.on("/tmp/jit.log")

Затем запустите ваш инструмент стресс-тестирования или несколько сотен наборов юнит-тестов, чтобы LuaJIT достаточно нагрелся для запуска JIT-компиляции. После этого проверьте результаты /tmp/jit.log.

Конечно, этот подход относительно утомителен, поэтому, если вы хотите упростить, resty достаточно, и CLI OpenResty поставляется со следующими опциями.

$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]

Где -j в resty — это опция, связанная с LuaJIT, значения dump и v следуют, соответствующие включению режимов jit.dump и jit.v.

В выводе модуля jit.v каждая строка — это успешно скомпилированный объект трассировки. Только что приведен пример JIT-способной трассировки, и если встречаются функции NYI, вывод укажет, что они являются NYI, как в примере следующего 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'

Она не может быть JIT-компилируемой, поэтому результат указывает на NYI-функцию в строке 8.

[TRACE 1 (command line -e):2 loop] [TRACE --- (command line -e):7 -- NYI: bytecode 72 at (command line -e):8]

Послесловие

Это первый раз, когда мы говорим о проблемах производительности OpenResty более подробно. После прочтения этих оптимизаций о NYI, что вы думаете? Вы можете оставить комментарий со своим мнением.

Наконец, я оставлю вам вопрос для размышления: при обсуждении альтернатив функции string.find() я упомянул, что было бы лучше сделать слой обертки и включить опции оптимизации по умолчанию. Таким образом, я оставлю эту задачу вам для небольшого тестового прогона.

Не стесняйтесь писать свои ответы в разделе комментариев, и вы можете поделиться этой статьей с вашими коллегами и друзьями, чтобы общаться и прогрессировать вместе.