В чем разница между LuaJIT и стандартным Lua?
API7.ai
September 23, 2022
Давайте узнаем о LuaJIT, еще одном краеугольном камне OpenResty, и я оставлю центральную часть сегодняшнего поста для некоторых важных и менее известных аспектов Lua и LuaJIT.
Вы можете узнать больше об основах Lua через поисковые системы или книги по Lua, и я рекомендую книгу Programming in Lua автора Lua.
Конечно, порог для написания правильного кода на LuaJIT в OpenResty не высок. Однако написание эффективного кода на LuaJIT — это не просто, и я подробно расскажу об этом в разделе оптимизации производительности OpenResty позже.
Давайте посмотрим, где LuaJIT находится в общей архитектуре OpenResty.

Как упоминалось ранее, процессы Worker в OpenResty создаются путем форка процесса Master. Виртуальная машина LuaJIT в процессе Master также форкается. Все процессы Worker внутри одного Worker используют эту виртуальную машину LuaJIT, и выполнение кода Lua происходит в этой виртуальной машине.
Это основы работы OpenResty, которые мы обсудим более подробно в последующих статьях. Сегодня мы начнем с того, что разберем отношения между Lua и LuaJIT.
Отношения между стандартным Lua и LuaJIT
Сначала разберем основные моменты.
Стандартный Lua и LuaJIT — это две разные вещи. LuaJIT совместим только с синтаксисом Lua 5.1.
Последняя версия стандартного Lua — 5.4.4, а последняя версия LuaJIT — 2.1.0-beta3. В более старых версиях OpenResty несколько лет назад можно было выбрать использование либо стандартной виртуальной машины Lua, либо виртуальной машины LuaJIT при компиляции, но сейчас поддержка стандартного Lua была удалена, и поддерживается только LuaJIT.
Синтаксис LuaJIT совместим с Lua 5.1, с опциональной поддержкой Lua 5.2 и 5.3. Поэтому мы должны сначала изучить синтаксис Lua 5.1, а затем на его основе изучить особенности LuaJIT. В предыдущей статье я познакомил вас с базовым синтаксисом Lua. Сегодня я упомяну только некоторые уникальные особенности Lua.
Стоит отметить, что OpenResty не использует напрямую официальную версию LuaJIT 2.1.0-beta3, а расширяет её своей форком: openresty-luajit2.
Эти уникальные API были добавлены в ходе реальной разработки OpenResty по соображениям производительности. Таким образом, LuaJIT, о котором мы говорим далее, относится к ветке LuaJIT, поддерживаемой самим OpenResty.
Почему LuaJIT?
После всего этого разговора об отношениях между LuaJIT и Lua, вы можете задаться вопросом, почему не использовать Lua напрямую, а использовать LuaJIT. На самом деле, основная причина — это преимущество в производительности LuaJIT.
Код Lua не интерпретируется напрямую, а компилируется в Byte Code компилятором Lua, а затем выполняется виртуальной машиной Lua.
Среда выполнения LuaJIT, помимо ассемблерной реализации интерпретатора Lua, имеет JIT-компилятор, который может генерировать машинный код напрямую. Сначала LuaJIT запускается как стандартный Lua, с кодом Lua, скомпилированным в байт-код, который интерпретируется и выполняется интерпретатором LuaJIT.
Разница в том, что интерпретатор LuaJIT записывает некоторые статистические данные во время выполнения байт-кода, такие как фактическое количество запусков каждой точки входа функции Lua и фактическое количество выполнений каждого цикла Lua. Когда эти счетчики превышают случайный порог, соответствующая точка входа функции Lua или цикл Lua считается достаточно "горячим", чтобы запустить JIT-компилятор.
JIT-компилятор пытается скомпилировать соответствующий путь кода Lua, начиная с точки входа "горячей" функции или места "горячего" цикла. Процесс компиляции преобразует байт-код LuaJIT в IR (Intermediate Representation), определенный LuaJIT, а затем генерирует машинный код для целевой архитектуры.
Таким образом, так называемая оптимизация производительности LuaJIT заключается в том, чтобы сделать как можно больше кода Lua доступным для генерации машинного код JIT-компилятором, а не возвращаться к режиму интерпретации интерпретатора Lua. Как только вы это поймете, вы сможете понять суть оптимизации производительности OpenResty, которую вы изучите позже.
Особенности Lua
Как описано в предыдущей статье, язык Lua относительно прост. Для инженеров с опытом работы на других языках программирования легко понять логику кода, если обратить внимание на некоторые уникальные аспекты Lua. Далее рассмотрим некоторые необычные аспекты языка Lua.
1. Индексация начинается с 1
Lua — единственный язык программирования, который я знаю, где индексация начинается с 1. Это, хотя и более понятно для людей без опыта программирования, может приводить к ошибкам в программах. Вот пример.
$ resty -e 't={100}; ngx.say(t[0])'
Вы могли бы ожидать, что программа выведет 100 или сообщит об ошибке, что индекс 0 не существует. Но, к удивлению, ничего не выводится, и ошибки не возникает. Давайте добавим команду type и посмотрим, что будет выведено.
$ resty -e 't={100};ngx.say(type(t[0]))' nil
Оказывается, это значение nil. На самом деле, в OpenResty определение и обработка значений nil также является запутанным моментом, поэтому мы поговорим об этом позже, когда будем обсуждать OpenResty.
2. Использование .. для конкатенации строк
В отличие от большинства языков, которые используют +, Lua использует две точки для конкатенации строк.
$ resty -e "ngx.say('hello' .. ', world')" hello, world
В реальной разработке проектов мы обычно используем несколько языков программирования, и необычный дизайн Lua всегда заставляет разработчиков задуматься, когда конкатенация строк вызывает затруднения.
3. Таблица — единственная структура данных
В отличие от Python, языка, богатого встроенными структурами данных, Lua имеет только одну структуру данных — table, которая может включать массивы и хэш-таблицы.
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
Если вы не присваиваете значение явно как пару ключ-значение, таблица по умолчанию использует числа в качестве индексов, начиная с 1. Поэтому color[1] — это blue.
Кроме того, получить правильную длину таблицы сложно, поэтому рассмотрим эти примеры.
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))
Результат:
Test1 3 Test2 2 Test3 1 Test4
Как видите, за исключением первого тестового случая, который возвращает длину 3, последующие тесты выходят за пределы наших ожиданий. На самом деле, чтобы получить длину таблицы в Lua, важно отметить, что правильное значение возвращается только если таблица является последовательностью.
Так что же такое последовательность? Во-первых, последовательность — это подмножество массива. То есть элементы таблицы доступны по положительному целочисленному индексу, и в таблице нет пар ключ-значение. В приведенном выше коде все таблицы, кроме t2, являются массивами.
Во-вторых, последовательность не содержит "дыр", то есть nil. Объединяя эти два пункта, таблица t1 является последовательностью, а t3 и t4 — массивами, но не последовательностями.
На этом этапе у вас может возникнуть вопрос, почему длина t4 будет 1? Это связано с тем, что при встрече с nil логика получения длины не продолжает выполняться, а возвращается сразу.
Не знаю, поняли ли вы это полностью. Эта часть действительно довольно сложная. Есть ли способ получить желаемую длину таблицы? Естественно, есть. OpenResty расширяет это, и я расскажу об этом позже в главе, посвященной таблицам, так что оставим интригу здесь.
4. Все переменные по умолчанию глобальные
Я хочу подчеркнуть, что если вы не уверены, всегда объявляйте новые переменные как local.
local s = 'hello'
Это связано с тем, что в Lua переменные по умолчанию являются глобальными и помещаются в таблицу с именем _G. Переменные, которые не являются локальными, ищутся в глобальной таблице, что является дорогостоящей операцией. Ошибки в написании имен переменных могут привести к ошибкам, которые трудно идентифицировать и исправить.
Поэтому в OpenResty я настоятельно рекомендую всегда объявлять переменные с помощью local, даже при подключении модуля.
-- Рекомендуется local xxx = require('xxx') -- Избегайте require('xxx')
LuaJIT
С учетом этих четырех особенностей Lua, перейдем к LuaJIT.
LuaJIT, помимо совместимости с Lua 5.1 и поддержки JIT, тесно интегрирован с FFI (Foreign Function Interface), что позволяет вам вызывать внешние функции C и использовать структуры данных C напрямую в вашем коде Lua. Вот простейший пример.
local ffi = require("ffi") ffi.cdef[[ int printf(const char *fmt, ...); ]] ffi.C.printf("Hello %s!", "world")
Всего в нескольких строках кода вы можете вызвать функцию printf из C напрямую из Lua и вывести Hello world! Вы можете использовать команду resty для запуска и посмотреть, работает ли это.
Аналогично, мы можем использовать FFI для вызова функций C из NGINX и OpenSSL, чтобы делать гораздо больше. Подход FFI работает лучше, чем традиционный подход Lua/C API, и именно поэтому существует проект lua-resty-core. В следующем разделе мы поговорим о FFI и lua-resty-core.
Кроме того, по соображениям производительности, LuaJIT расширяет функции таблиц: table.new и table.clear, две важные функции оптимизации производительности, часто используемые в библиотеке lua-resty OpenResty. Однако немногие разработчики знакомы с ними, так как документация сложна, и нет примеров кода. Мы оставим их для раздела оптимизации производительности.
Итог
Давайте подведем итог сегодняшнего материала.
OpenResty выбирает LuaJIT, а не стандартный Lua, по соображениям производительности, и поддерживает свою ветку LuaJIT. LuaJIT основан на синтаксисе Lua 5.1 и избирательно совместим с некоторыми синтаксисами Lua 5.2 и Lua 5.3, чтобы сформировать свою систему. Что касается синтаксиса Lua, который вам нужно освоить, он имеет свои отличительные особенности в индексации, конкатенации строк, структурах данных и переменных, на которые следует обращать особое внимание при написании кода.
Сталкивались ли вы с какими-либо подводными камнями при изучении Lua и LuaJIT? Поделитесь своими мнениями с нами, и я написал пост, чтобы поделиться своими ошибками. Вы также можете поделиться этим постом с коллегами и друзьями, чтобы учиться и развиваться вместе.