Что такое Rspamd
• Система фильтрации спама с ориентацией на производительность
• OpenSource проект
• Плагины и правила написаны на языке Lua
• Ядро написано на plain (old) C
Lua и C
В Rspamd
Lua
C
Соотношение Lua и C
По строкам кода
0
35000
70000
105000
140000
Plain C
Lua
Assembly Other
Perl
Соотношение Lua и C
По количеству плагинов
0
8
15
23
30
6
26
Lua C
Что написано на Lua
• Правила, а также комбинации регулярных выражений
• Большая часть плагинов:
• Проверка DNS списков
• Взаимодействие с Redis (репутация, “серые” списки, динамические настройки, список ответов,
динамические лимиты)
• Работа с внешними сервисами (антивирусы, запись в ClickHouse итд)
• Контент-фильтрация
• Работа с нейросетями и классификацией текстов
• Работа с SpamAssassin правилами
Почему Lua?
Экскурс в историю
K&R определение функции
Странный синтаксис переменных
Что это?
Надо больше макросов!
Похоже, это обычный C
Правило на Lua
Встраиваемый язык
Каким он должен быть
Встраиваемый язык
Каким он должен быть
• Минимум внешних зависимостей (JavaScript)
Встраиваемый язык
Каким он должен быть
• Минимум внешних зависимостей (JavaScript)
• Простота синтаксиса (Perl)
Встраиваемый язык
Каким он должен быть
• Минимум внешних зависимостей (JavaScript)
• Простота синтаксиса (Perl)
• Скорость работы (Python, Guile, TCL)
Встраиваемый язык
Каким он должен быть
• Минимум внешних зависимостей (JavaScript)
• Простота синтаксиса (Perl)
• Скорость работы (Python, Guile, TCL)
• Скорость переключения между C и встраиваемым языком
Что дал переход на Lua
Что дал переход на Lua
• Появилось несколько активных авторов плагинов и правил на Lua
Что дал переход на Lua
• Появилось несколько активных авторов плагинов и правил на Lua
• Появились юнит-тесты на Lua
Что дал переход на Lua
• Появилось несколько активных авторов плагинов и правил на Lua
• Появились юнит-тесты на Lua
• Текущий подход к разработке: писать Lua биндинги на Си, а основную
логику на Lua
Что дал переход на Lua
• Появилось несколько активных авторов плагинов и правил на Lua
• Появились юнит-тесты на Lua
• Текущий подход к разработке: писать Lua биндинги на Си, а основную
логику на Lua
• Упростилось написание утилит командной строки
Особенности Lua
Кривая обучения Lua
Начал писать на Lua
Обучение Lua
That was my first ever attempt at Lua, I asked you to add halfOpen
support to your lua_tcp module, which you did within about 30 mins
and I had the plugin written an working within an hour or so.
Обучение Lua
• Плагин опроса внешнего сервиса DCC:
That was my first ever attempt at Lua, I asked you to add halfOpen
support to your lua_tcp module, which you did within about 30 mins
and I had the plugin written an working within an hour or so.
Обучение Lua
• Плагин опроса внешнего сервиса DCC:
• написан за час времени
That was my first ever attempt at Lua, I asked you to add halfOpen
support to your lua_tcp module, which you did within about 30 mins
and I had the plugin written an working within an hour or so.
Обучение Lua
• Плагин опроса внешнего сервиса DCC:
• написан за час времени
• человеком, без знаний Lua
That was my first ever attempt at Lua, I asked you to add halfOpen
support to your lua_tcp module, which you did within about 30 mins
and I had the plugin written an working within an hour or so.
Обучение Lua
• Плагин опроса внешнего сервиса DCC:
• написан за час времени
• человеком, без знаний Lua
• еще 30 минут заняла адаптация биндингов на C
That was my first ever attempt at Lua, I asked you to add halfOpen
support to your lua_tcp module, which you did within about 30 mins
and I had the plugin written an working within an hour or so.
Обучение Lua
• Плагин опроса внешнего сервиса DCC:
• написан за час времени
• человеком, без знаний Lua
• еще 30 минут заняла адаптация биндингов на C
• аналог на перле - больше 1k строк кода
That was my first ever attempt at Lua, I asked you to add halfOpen
support to your lua_tcp module, which you did within about 30 mins
and I had the plugin written an working within an hour or so.
Некоторые особенности Lua
Общие характеристики языка
Некоторые особенности Lua
Общие характеристики языка
• Крайне простой синтаксис (20 правил в BNF грамматике)
Некоторые особенности Lua
Общие характеристики языка
• Крайне простой синтаксис (20 правил в BNF грамматике)
• Таблицы - универсальный инструмент работы с данными
Некоторые особенности Lua
Общие характеристики языка
• Крайне простой синтаксис (20 правил в BNF грамматике)
• Таблицы - универсальный инструмент работы с данными
• Функции - объекты первого рода (возможны функциональные
конструкции и замыкания)
Некоторые особенности Lua
Общие характеристики языка
• Крайне простой синтаксис (20 правил в BNF грамматике)
• Таблицы - универсальный инструмент работы с данными
• Функции - объекты первого рода (возможны функциональные
конструкции и замыкания)
• Стандартная библиотека содержит только необходимый минимум
функций
Некоторые особенности Lua
Общие характеристики языка
• Крайне простой синтаксис (20 правил в BNF грамматике)
• Таблицы - универсальный инструмент работы с данными
• Функции - объекты первого рода (возможны функциональные
конструкции и замыкания)
• Стандартная библиотека содержит только необходимый минимум
функций
• Динамическая строгая типизация
Синтаксис Lua
Основные элементы
Синтаксис Lua
Основные элементы
• Переменные:
Синтаксис Lua
Основные элементы
• Переменные: local ret = false -- Generic variable
local rules = {} -- Empty table
local rspamd_logger = require “rspamd_logger" -- Load rspamd module
Синтаксис Lua
Основные элементы
• Переменные:
• Условия:
local ret = false -- Generic variable
local rules = {} -- Empty table
local rspamd_logger = require “rspamd_logger" -- Load rspamd module
Синтаксис Lua
Основные элементы
• Переменные:
• Условия:
local ret = false -- Generic variable
local rules = {} -- Empty table
local rspamd_logger = require “rspamd_logger" -- Load rspamd module
if not ret then -- can use ‘not’, ‘and’, ‘or’ here
…
elseif ret ~= 10 then -- note ~= for ‘not equal’ operator
end
Синтаксис Lua
Основные элементы
• Переменные:
• Условия:
• Циклы:
local ret = false -- Generic variable
local rules = {} -- Empty table
local rspamd_logger = require “rspamd_logger" -- Load rspamd module
if not ret then -- can use ‘not’, ‘and’, ‘or’ here
…
elseif ret ~= 10 then -- note ~= for ‘not equal’ operator
end
Синтаксис Lua
Основные элементы
• Переменные:
• Условия:
• Циклы:
local ret = false -- Generic variable
local rules = {} -- Empty table
local rspamd_logger = require “rspamd_logger" -- Load rspamd module
if not ret then -- can use ‘not’, ‘and’, ‘or’ here
…
elseif ret ~= 10 then -- note ~= for ‘not equal’ operator
end
for k,m in pairs(opts) do … end -- Iterate over keyed table a[‘key’] =
value
for _,i in ipairs(images) do … end -- Iterate over array table a[1] =
value
for i=1,10 do … end -- Count from 1 to 10
Синтаксис Lua
Основные элементы
• Переменные:
• Условия:
• Циклы:
• Таблицы:
local ret = false -- Generic variable
local rules = {} -- Empty table
local rspamd_logger = require “rspamd_logger" -- Load rspamd module
if not ret then -- can use ‘not’, ‘and’, ‘or’ here
…
elseif ret ~= 10 then -- note ~= for ‘not equal’ operator
end
for k,m in pairs(opts) do … end -- Iterate over keyed table a[‘key’] =
value
for _,i in ipairs(images) do … end -- Iterate over array table a[1] =
value
for i=1,10 do … end -- Count from 1 to 10
Синтаксис Lua
Основные элементы
• Переменные:
• Условия:
• Циклы:
• Таблицы:
local ret = false -- Generic variable
local rules = {} -- Empty table
local rspamd_logger = require “rspamd_logger" -- Load rspamd module
if not ret then -- can use ‘not’, ‘and’, ‘or’ here
…
elseif ret ~= 10 then -- note ~= for ‘not equal’ operator
end
for k,m in pairs(opts) do … end -- Iterate over keyed table a[‘key’] =
value
for _,i in ipairs(images) do … end -- Iterate over array table a[1] =
value
for i=1,10 do … end -- Count from 1 to 10
local options = { [1] = ‘value’, [‘key’] = 1, -- Numbers starts from 1
another_key = function(task) … end, -- Functions can be values
[2] = {} -- Other tables can be values
} -- Can have both numbers and strings as key and anything as values
Синтаксис Lua
Основные элементы
• Переменные:
• Условия:
• Циклы:
• Таблицы:
• Функции:
local ret = false -- Generic variable
local rules = {} -- Empty table
local rspamd_logger = require “rspamd_logger" -- Load rspamd module
if not ret then -- can use ‘not’, ‘and’, ‘or’ here
…
elseif ret ~= 10 then -- note ~= for ‘not equal’ operator
end
for k,m in pairs(opts) do … end -- Iterate over keyed table a[‘key’] =
value
for _,i in ipairs(images) do … end -- Iterate over array table a[1] =
value
for i=1,10 do … end -- Count from 1 to 10
local options = { [1] = ‘value’, [‘key’] = 1, -- Numbers starts from 1
another_key = function(task) … end, -- Functions can be values
[2] = {} -- Other tables can be values
} -- Can have both numbers and strings as key and anything as values
Синтаксис Lua
Основные элементы
• Переменные:
• Условия:
• Циклы:
• Таблицы:
• Функции:
local ret = false -- Generic variable
local rules = {} -- Empty table
local rspamd_logger = require “rspamd_logger" -- Load rspamd module
if not ret then -- can use ‘not’, ‘and’, ‘or’ here
…
elseif ret ~= 10 then -- note ~= for ‘not equal’ operator
end
for k,m in pairs(opts) do … end -- Iterate over keyed table a[‘key’] =
value
for _,i in ipairs(images) do … end -- Iterate over array table a[1] =
value
for i=1,10 do … end -- Count from 1 to 10
local options = { [1] = ‘value’, [‘key’] = 1, -- Numbers starts from 1
another_key = function(task) … end, -- Functions can be values
[2] = {} -- Other tables can be values
} -- Can have both numbers and strings as key and anything as values
local function something(task) -- Normal definition
local cb = function(data) -- Functions can be nested
…
end
end
Таблицы в Lua
Таблицы в Lua
Основные определения
Таблицы в Lua
Основные определения
• Значения - все, кроме nil:
Таблицы в Lua
Основные определения
• Значения - все, кроме nil: local t = {
1, -- number
'test', -- string
function() end, -- function
{1, 2, 3} -- another table
task:get_mempool(), -- userdata
}
Таблицы в Lua
Основные определения
• Значения - все, кроме nil: local t = {
1, -- number
'test', -- string
function() end, -- function
{1, 2, 3} -- another table
task:get_mempool(), -- userdata
}
• Ключи - строки и числа:
Таблицы в Lua
Основные определения
• Значения - все, кроме nil: local t = {
1, -- number
'test', -- string
function() end, -- function
{1, 2, 3} -- another table
task:get_mempool(), -- userdata
}
local t = {
1, -- 1
[3] = {1,2},
test = function() end,
['spaces in key'] = 'abc',
[1.2] = 3,
[-1] = 4,
}
• Ключи - строки и числа:
Таблицы в Lua
table
local table = {
a = 1,
b = 2,
[1] = 3,
[2] = function() end,
[0] = true
}
3
function() end
true
1
2
1
2
0
‘a’
‘b’
Массив Хеш таблица
!
Таблицы в Lua
Итерация
local t = {
a = 1,
b = 2,
[1] = 3,
[2] = function() end,
[0] = true
}
Массив
Таблица
целиком
1..2
Метатаблицы
Когда просто таблиц недостаточно
• Задают общие свойства для других таблиц
• Примерно соответствуют Prototype в JavaScript
• Позволяют задавать функции-методы для таблиц (через __newindex или __index)
• Могут также задавать операторы над таблицами (например, через __eq или
__sum)
• Можно задавать метатаблицы для стандартных Lua типов (например, строк)
• Используются в C API для определения методов для типа userdata
Функции в Lua
Функции в Lua
Основные элементы
Функции в Lua
Основные элементы
• Могут быть вложенными:
Функции в Lua
Основные элементы
• Могут быть вложенными:
function foo(n) -- global function
local var = function(m) -- function in var
return m + n -- n is from `foo`
end
local function bar() -- another form of var
return var(2)
end
return bar()
end
Функции в Lua
Основные элементы
• Могут быть вложенными:
function foo(n) -- global function
local var = function(m) -- function in var
return m + n -- n is from `foo`
end
local function bar() -- another form of var
return var(2)
end
return bar()
end
• Могут быть аргументами и
возвращаться из функций:
Функции в Lua
Основные элементы
• Могут быть вложенными:
function foo(n) -- global function
local var = function(m) -- function in var
return m + n -- n is from `foo`
end
local function bar() -- another form of var
return var(2)
end
return bar()
end
local function f(cb) -- accepts callback function
return function(args) -- accepts some args
return cb(args) -- return callback function from args (decorator like)
end
end
• Могут быть аргументами и
возвращаться из функций:
Функции в Lua
Замыкания
function test(n)
local f = function()
return n + 1 -- n is captured
end
n = n + 1 -- n in f is also modified
return а
end
test(1) -- Returns function closure
test(1)() -- Returns 3
Функции в Lua
Замыкания
function test(n)
local f = function()
return n + 1 -- n is captured
end
n = n + 1 -- n in f is also modified
return а
end
test(1) -- Returns function closure
test(1)() -- Returns 3
Замыкание
Функции в Lua
Замыкания
function test(n)
local f = function()
return n + 1 -- n is captured
end
n = n + 1 -- n in f is also modified
return а
end
test(1) -- Returns function closure
test(1)() -- Returns 3
Замыкание
Функции в Lua
Замыкания
function test(n)
local f = function()
return n + 1 -- n is captured
end
n = n + 1 -- n in f is also modified
return а
end
test(1) -- Returns function closure
test(1)() -- Returns 3
Замыкание
Переменные входят по ссылке
Функции в Lua
Замыкания
function test(n)
local f = function()
return n + 1 -- n is captured
end
n = n + 1 -- n in f is also modified
return а
end
test(1) -- Returns function closure
test(1)() -- Returns 3
Замыкание
Переменные входят по ссылке
Можно возвращать функцию
Функции в Lua
Замыкания
function test(n)
local f = function()
return n + 1 -- n is captured
end
n = n + 1 -- n in f is also modified
return а
end
test(1) -- Returns function closure
test(1)() -- Returns 3
Замыкание
Переменные входят по ссылке
Функция-замыкание
Можно возвращать функцию
Функции в Lua
Замыкания
function test(n)
local f = function()
return n + 1 -- n is captured
end
n = n + 1 -- n in f is also modified
return а
end
test(1) -- Returns function closure
test(1)() -- Returns 3
Замыкание
Переменные входят по ссылке
Функция-замыкание
Непосредственно вызов
Можно возвращать функцию
Функции в Lua
• Позволяют писать в функциональном стиле (библиотека lua-fun)
• Замыкания работают также для C функций
• Передача по ссылке несколько необычна, но эффективна
• Время жизни замыкания ассоциируется с переменной, в которой оно
хранится
Строки в Lua
Строки в Lua
Передача строки из C
Глобальный хеш
строк
s1
s2
s3
lua_pushstring (L, "hello");
hello
Копия
Хеш
hash(hello)
Поиск
Строки в Lua
Передача строки из C
Глобальный хеш
строк
s1
s2
s3
lua_pushstring (L, "hello");
hello
Вставка
hello
Строки в Lua
• Строки в Lua неизменяемы
• Сравнение строк - просто сравнение указателей: O(1)
• Создание и передача строк - дорогая операция
• Если нужно делать строку из кусков, то нужно использовать таблицу
и table.concat
Строки в Lua
Секунды(меньше-лучше)
0
75
150
225
300
Naive concat Table.concat
6.7c
274c
40x
Строки в Lua
Передача текста
C C
Userdata
const char *s
size_t len
refcounter
Взаимодействие Lua и C
Виртуальный стек
Общий вид
1
2
3
4
-4
-3
-2
-1 lua_gettop() ⟹4
PushPop
Виртуальный стек
Добавление значений
1
2
3
4
-6
-5
-4
-3
lua_pushnumbernumber
string
5
6
-2
-1 lua_pushstring
Push
Виртуальный стек
Извлечение значений
1
2
3
4
-3
-2
-1
lua_pop(L, 3);number
string
5
6
Pop
Виртуальный стек
Вызов функции
1
2
3
4
-3
-2
-1
5
6
Внутренние переменые
number
string
4
5
-2
-1
Аргументы
Возвращаемые значения
Проблемы работы со стеком
Проблемы работы со стеком
• Скорость: переключение занимает около сотни CPU cycles
Проблемы работы со стеком
• Скорость: переключение занимает около сотни CPU cycles
• Проблемы контроля: без специальных ключей компиляции легко
получить низкоуровневые ошибки (вплоть до полного падения)
Проблемы работы со стеком
• Скорость: переключение занимает около сотни CPU cycles
• Проблемы контроля: без специальных ключей компиляции легко
получить низкоуровневые ошибки (вплоть до полного падения)
Проблемы работы со стеком
• Скорость: переключение занимает около сотни CPU cycles
• Проблемы контроля: без специальных ключей компиляции легко
получить низкоуровневые ошибки (вплоть до полного падения)
• Крайне неочевидный код
Проблемы работы со стеком
• Скорость: переключение занимает около сотни CPU cycles
• Проблемы контроля: без специальных ключей компиляции легко
получить низкоуровневые ошибки (вплоть до полного падения)
• Крайне неочевидный код
• Есть ряд сложных моментов (итерация по таблице)
Пример из практики
Итерация по таблице
lua_pushvalue (L, i); /* Push table on top */
lua_pushnil (L); /* Push nil to start iterate */
while (lua_next (L, -2)) {
lua_pushvalue (L, -2); /* Copy key as it is special */
key = luaL_checkstring (L, -1);
value = luaL_checkstring (L, -2);
lua_pop (L, 2); /* Remove key and value leaving original key */
}
lua_pop (L, 1); /* Remove table */
Пример из практики
Итерация по таблице
lua_pushvalue (L, i); tabletop -1
Пример из практики
Итерация по таблице
lua_pushvalue (L, i);
lua_pushnil (L);
tabletop -2
nil -1
Пример из практики
Итерация по таблице
table -2
nil -1
while (lua_next (L, -2))
table -3
key -2
value -1
Предыдущий ключ
Новый ключ
Пример из практики
Итерация по таблице
lua_pushvalue (L, -2);
key = luaL_checkstring (L, -1);
value = luaL_checkstring (L, -2);
table
-2
key
-1
value
key copy
-3
-4
Пример из практики
Итерация по таблице
lua_pop (L, 2);
table
-2
key
-1
value
key copy
-3
-4
table
key -1
-2
while (lua_next (L, -2))
Используется для следующей итерации
FFI вызовы кода на C
Плюсы FFI
• Очень быстро работают для простых типов (десяток циклов)
• Поддерживают все конструкции C99
• Поддерживают типы-обертки (boxed types), например 64-х битные
целые
• Проще поддерживать
FFI вызовы
Синтетические тесты
Секунды(меньше-лучше)
0
10
20
30
40
tanh(x) strcmp(s1, s2)
Plain call FFI Plain call FFI
5.97c
3.13c
4.38c
30.07c
10x
Пример из практики
Оптимизация узких мест
local ffi
if type(jit) == 'table' then
ffi = require("ffi")
ffi.cdef[[
int rspamd_re_cache_type_from_string (const char *str);
int rspamd_re_cache_process_ffi (void *ptask,
void *pre,
int type,
const char *type_data,
int is_strong);
]]
end
local function process_regexp_opt(re, task, re_type, header, strong)
if type(jit) == 'table' then
-- Use ffi call
local itype = ffi.C.rspamd_re_cache_type_from_string(re_type)
if not strong then
strong = 0
else
string = 1
end
local iret = ffi.C.rspamd_re_cache_process_ffi (task, re, itype, header, strong)
return tonumber(iret)
else
return task:process_regexp(re, re_type, header, strong)
end
end
Проверка
LuaJIT
local ffi
if type(jit) == 'table' then
ffi = require("ffi")
ffi.cdef[[
int rspamd_re_cache_type_from_string (const char *str);
int rspamd_re_cache_process_ffi (void *ptask,
void *pre,
int type,
const char *type_data,
int is_strong);
]]
end
local function process_regexp_opt(re, task, re_type, header, strong)
if type(jit) == 'table' then
-- Use ffi call
local itype = ffi.C.rspamd_re_cache_type_from_string(re_type)
if not strong then
strong = 0
else
string = 1
end
local iret = ffi.C.rspamd_re_cache_process_ffi (task, re, itype, header, strong)
return tonumber(iret)
else
return task:process_regexp(re, re_type, header, strong)
end
end
Определение С функций
Пример из практики
Оптимизация узких мест
local ffi
if type(jit) == 'table' then
ffi = require("ffi")
ffi.cdef[[
int rspamd_re_cache_type_from_string (const char *str);
int rspamd_re_cache_process_ffi (void *ptask,
void *pre,
int type,
const char *type_data,
int is_strong);
]]
end
local function process_regexp_opt(re, task, re_type, header, strong)
if type(jit) == 'table' then
-- Use ffi call
local itype = ffi.C.rspamd_re_cache_type_from_string(re_type)
if not strong then
strong = 0
else
string = 1
end
local iret = ffi.C.rspamd_re_cache_process_ffi (task, re, itype, header, strong)
return tonumber(iret)
else
return task:process_regexp(re, re_type, header, strong)
end
end
FFI call
Userdata to C pointer
bool -> int
Пример из практики
Оптимизация узких мест
local ffi
if type(jit) == 'table' then
ffi = require("ffi")
ffi.cdef[[
int rspamd_re_cache_type_from_string (const char *str);
int rspamd_re_cache_process_ffi (void *ptask,
void *pre,
int type,
const char *type_data,
int is_strong);
]]
end
local function process_regexp_opt(re, task, re_type, header, strong)
if type(jit) == 'table' then
-- Use ffi call
local itype = ffi.C.rspamd_re_cache_type_from_string(re_type)
if not strong then
strong = 0
else
string = 1
end
local iret = ffi.C.rspamd_re_cache_process_ffi (task, re, itype, header, strong)
return tonumber(iret)
else
return task:process_regexp(re, re_type, header, strong)
end
end
Plain call
Пример из практики
Оптимизация узких мест
Оптимизация FFI вызовов
Lua вызов
FFI вызов
Проблемы FFI
Проблемы FFI
• Поддерживаются на ограниченном количестве архитектур (Sparc64)
Проблемы FFI
• Поддерживаются на ограниченном количестве архитектур (Sparc64)
• В plain Lua требуют отдельного модуля FFI, LuaJIT поддерживает “из
коробки”
Проблемы FFI
• Поддерживаются на ограниченном количестве архитектур (Sparc64)
• В plain Lua требуют отдельного модуля FFI, LuaJIT поддерживает “из
коробки”
• Нет проверки типов, нет контроля целостности памяти
Проблемы FFI
• Поддерживаются на ограниченном количестве архитектур (Sparc64)
• В plain Lua требуют отдельного модуля FFI, LuaJIT поддерживает “из
коробки”
• Нет проверки типов, нет контроля целостности памяти
Проблемы FFI
• Поддерживаются на ограниченном количестве архитектур (Sparc64)
• В plain Lua требуют отдельного модуля FFI, LuaJIT поддерживает “из
коробки”
• Нет проверки типов, нет контроля целостности памяти
Проблемы FFI
• Поддерживаются на ограниченном количестве архитектур (Sparc64)
• В plain Lua требуют отдельного модуля FFI, LuaJIT поддерживает “из
коробки”
• Нет проверки типов, нет контроля целостности памяти
• Не всегда быстрее
Проблемы FFI
• Поддерживаются на ограниченном количестве архитектур (Sparc64)
• В plain Lua требуют отдельного модуля FFI, LuaJIT поддерживает “из
коробки”
• Нет проверки типов, нет контроля целостности памяти
• Не всегда быстрее
• Очень сложно сделать sandbox
Выводы
Выводы
Выводы
• Lua простой для изучения
Выводы
• Lua простой для изучения
Выводы
• Lua простой для изучения
• Lua удобен для встраивания
Выводы
• Lua простой для изучения
• Lua удобен для встраивания
Выводы
Выводы
• Основной инструмент - таблицы
Выводы
• Основной инструмент - таблицы
Выводы
• Основной инструмент - таблицы
• Основной инструмент - функции
Выводы
• Основной инструмент - таблицы
• Основной инструмент - функции
Выводы
Выводы
• Lua - быстрый язык (особенно LuaJIT)
Выводы
• Lua - быстрый язык (особенно LuaJIT)
Выводы
• Lua - быстрый язык (особенно LuaJIT)
• Некоторые оптимизации бывают опасны
Выводы
• Lua - быстрый язык (особенно LuaJIT)
• Некоторые оптимизации бывают опасны
Всеволод Стахов
vstakhov@rspamd.com
Вопросы

Практика совместного использования Lua и C в opensource спам-фильтре Rspamd / Всеволод Стахов (University of Cambridge, Mimecast)

  • 2.
    Что такое Rspamd •Система фильтрации спама с ориентацией на производительность • OpenSource проект • Плагины и правила написаны на языке Lua • Ядро написано на plain (old) C
  • 3.
    Lua и C ВRspamd Lua C
  • 4.
    Соотношение Lua иC По строкам кода 0 35000 70000 105000 140000 Plain C Lua Assembly Other Perl
  • 5.
    Соотношение Lua иC По количеству плагинов 0 8 15 23 30 6 26 Lua C
  • 6.
    Что написано наLua • Правила, а также комбинации регулярных выражений • Большая часть плагинов: • Проверка DNS списков • Взаимодействие с Redis (репутация, “серые” списки, динамические настройки, список ответов, динамические лимиты) • Работа с внешними сервисами (антивирусы, запись в ClickHouse итд) • Контент-фильтрация • Работа с нейросетями и классификацией текстов • Работа с SpamAssassin правилами
  • 7.
  • 8.
  • 9.
    K&R определение функции Странныйсинтаксис переменных Что это? Надо больше макросов!
  • 10.
  • 11.
  • 12.
  • 13.
    Встраиваемый язык Каким ондолжен быть • Минимум внешних зависимостей (JavaScript)
  • 14.
    Встраиваемый язык Каким ондолжен быть • Минимум внешних зависимостей (JavaScript) • Простота синтаксиса (Perl)
  • 15.
    Встраиваемый язык Каким ондолжен быть • Минимум внешних зависимостей (JavaScript) • Простота синтаксиса (Perl) • Скорость работы (Python, Guile, TCL)
  • 16.
    Встраиваемый язык Каким ондолжен быть • Минимум внешних зависимостей (JavaScript) • Простота синтаксиса (Perl) • Скорость работы (Python, Guile, TCL) • Скорость переключения между C и встраиваемым языком
  • 17.
  • 18.
    Что дал переходна Lua • Появилось несколько активных авторов плагинов и правил на Lua
  • 19.
    Что дал переходна Lua • Появилось несколько активных авторов плагинов и правил на Lua • Появились юнит-тесты на Lua
  • 20.
    Что дал переходна Lua • Появилось несколько активных авторов плагинов и правил на Lua • Появились юнит-тесты на Lua • Текущий подход к разработке: писать Lua биндинги на Си, а основную логику на Lua
  • 21.
    Что дал переходна Lua • Появилось несколько активных авторов плагинов и правил на Lua • Появились юнит-тесты на Lua • Текущий подход к разработке: писать Lua биндинги на Си, а основную логику на Lua • Упростилось написание утилит командной строки
  • 22.
  • 23.
  • 24.
    Обучение Lua That wasmy first ever attempt at Lua, I asked you to add halfOpen support to your lua_tcp module, which you did within about 30 mins and I had the plugin written an working within an hour or so.
  • 25.
    Обучение Lua • Плагинопроса внешнего сервиса DCC: That was my first ever attempt at Lua, I asked you to add halfOpen support to your lua_tcp module, which you did within about 30 mins and I had the plugin written an working within an hour or so.
  • 26.
    Обучение Lua • Плагинопроса внешнего сервиса DCC: • написан за час времени That was my first ever attempt at Lua, I asked you to add halfOpen support to your lua_tcp module, which you did within about 30 mins and I had the plugin written an working within an hour or so.
  • 27.
    Обучение Lua • Плагинопроса внешнего сервиса DCC: • написан за час времени • человеком, без знаний Lua That was my first ever attempt at Lua, I asked you to add halfOpen support to your lua_tcp module, which you did within about 30 mins and I had the plugin written an working within an hour or so.
  • 28.
    Обучение Lua • Плагинопроса внешнего сервиса DCC: • написан за час времени • человеком, без знаний Lua • еще 30 минут заняла адаптация биндингов на C That was my first ever attempt at Lua, I asked you to add halfOpen support to your lua_tcp module, which you did within about 30 mins and I had the plugin written an working within an hour or so.
  • 29.
    Обучение Lua • Плагинопроса внешнего сервиса DCC: • написан за час времени • человеком, без знаний Lua • еще 30 минут заняла адаптация биндингов на C • аналог на перле - больше 1k строк кода That was my first ever attempt at Lua, I asked you to add halfOpen support to your lua_tcp module, which you did within about 30 mins and I had the plugin written an working within an hour or so.
  • 30.
    Некоторые особенности Lua Общиехарактеристики языка
  • 31.
    Некоторые особенности Lua Общиехарактеристики языка • Крайне простой синтаксис (20 правил в BNF грамматике)
  • 32.
    Некоторые особенности Lua Общиехарактеристики языка • Крайне простой синтаксис (20 правил в BNF грамматике) • Таблицы - универсальный инструмент работы с данными
  • 33.
    Некоторые особенности Lua Общиехарактеристики языка • Крайне простой синтаксис (20 правил в BNF грамматике) • Таблицы - универсальный инструмент работы с данными • Функции - объекты первого рода (возможны функциональные конструкции и замыкания)
  • 34.
    Некоторые особенности Lua Общиехарактеристики языка • Крайне простой синтаксис (20 правил в BNF грамматике) • Таблицы - универсальный инструмент работы с данными • Функции - объекты первого рода (возможны функциональные конструкции и замыкания) • Стандартная библиотека содержит только необходимый минимум функций
  • 35.
    Некоторые особенности Lua Общиехарактеристики языка • Крайне простой синтаксис (20 правил в BNF грамматике) • Таблицы - универсальный инструмент работы с данными • Функции - объекты первого рода (возможны функциональные конструкции и замыкания) • Стандартная библиотека содержит только необходимый минимум функций • Динамическая строгая типизация
  • 36.
  • 37.
  • 38.
    Синтаксис Lua Основные элементы •Переменные: local ret = false -- Generic variable local rules = {} -- Empty table local rspamd_logger = require “rspamd_logger" -- Load rspamd module
  • 39.
    Синтаксис Lua Основные элементы •Переменные: • Условия: local ret = false -- Generic variable local rules = {} -- Empty table local rspamd_logger = require “rspamd_logger" -- Load rspamd module
  • 40.
    Синтаксис Lua Основные элементы •Переменные: • Условия: local ret = false -- Generic variable local rules = {} -- Empty table local rspamd_logger = require “rspamd_logger" -- Load rspamd module if not ret then -- can use ‘not’, ‘and’, ‘or’ here … elseif ret ~= 10 then -- note ~= for ‘not equal’ operator end
  • 41.
    Синтаксис Lua Основные элементы •Переменные: • Условия: • Циклы: local ret = false -- Generic variable local rules = {} -- Empty table local rspamd_logger = require “rspamd_logger" -- Load rspamd module if not ret then -- can use ‘not’, ‘and’, ‘or’ here … elseif ret ~= 10 then -- note ~= for ‘not equal’ operator end
  • 42.
    Синтаксис Lua Основные элементы •Переменные: • Условия: • Циклы: local ret = false -- Generic variable local rules = {} -- Empty table local rspamd_logger = require “rspamd_logger" -- Load rspamd module if not ret then -- can use ‘not’, ‘and’, ‘or’ here … elseif ret ~= 10 then -- note ~= for ‘not equal’ operator end for k,m in pairs(opts) do … end -- Iterate over keyed table a[‘key’] = value for _,i in ipairs(images) do … end -- Iterate over array table a[1] = value for i=1,10 do … end -- Count from 1 to 10
  • 43.
    Синтаксис Lua Основные элементы •Переменные: • Условия: • Циклы: • Таблицы: local ret = false -- Generic variable local rules = {} -- Empty table local rspamd_logger = require “rspamd_logger" -- Load rspamd module if not ret then -- can use ‘not’, ‘and’, ‘or’ here … elseif ret ~= 10 then -- note ~= for ‘not equal’ operator end for k,m in pairs(opts) do … end -- Iterate over keyed table a[‘key’] = value for _,i in ipairs(images) do … end -- Iterate over array table a[1] = value for i=1,10 do … end -- Count from 1 to 10
  • 44.
    Синтаксис Lua Основные элементы •Переменные: • Условия: • Циклы: • Таблицы: local ret = false -- Generic variable local rules = {} -- Empty table local rspamd_logger = require “rspamd_logger" -- Load rspamd module if not ret then -- can use ‘not’, ‘and’, ‘or’ here … elseif ret ~= 10 then -- note ~= for ‘not equal’ operator end for k,m in pairs(opts) do … end -- Iterate over keyed table a[‘key’] = value for _,i in ipairs(images) do … end -- Iterate over array table a[1] = value for i=1,10 do … end -- Count from 1 to 10 local options = { [1] = ‘value’, [‘key’] = 1, -- Numbers starts from 1 another_key = function(task) … end, -- Functions can be values [2] = {} -- Other tables can be values } -- Can have both numbers and strings as key and anything as values
  • 45.
    Синтаксис Lua Основные элементы •Переменные: • Условия: • Циклы: • Таблицы: • Функции: local ret = false -- Generic variable local rules = {} -- Empty table local rspamd_logger = require “rspamd_logger" -- Load rspamd module if not ret then -- can use ‘not’, ‘and’, ‘or’ here … elseif ret ~= 10 then -- note ~= for ‘not equal’ operator end for k,m in pairs(opts) do … end -- Iterate over keyed table a[‘key’] = value for _,i in ipairs(images) do … end -- Iterate over array table a[1] = value for i=1,10 do … end -- Count from 1 to 10 local options = { [1] = ‘value’, [‘key’] = 1, -- Numbers starts from 1 another_key = function(task) … end, -- Functions can be values [2] = {} -- Other tables can be values } -- Can have both numbers and strings as key and anything as values
  • 46.
    Синтаксис Lua Основные элементы •Переменные: • Условия: • Циклы: • Таблицы: • Функции: local ret = false -- Generic variable local rules = {} -- Empty table local rspamd_logger = require “rspamd_logger" -- Load rspamd module if not ret then -- can use ‘not’, ‘and’, ‘or’ here … elseif ret ~= 10 then -- note ~= for ‘not equal’ operator end for k,m in pairs(opts) do … end -- Iterate over keyed table a[‘key’] = value for _,i in ipairs(images) do … end -- Iterate over array table a[1] = value for i=1,10 do … end -- Count from 1 to 10 local options = { [1] = ‘value’, [‘key’] = 1, -- Numbers starts from 1 another_key = function(task) … end, -- Functions can be values [2] = {} -- Other tables can be values } -- Can have both numbers and strings as key and anything as values local function something(task) -- Normal definition local cb = function(data) -- Functions can be nested … end end
  • 47.
  • 48.
  • 49.
    Таблицы в Lua Основныеопределения • Значения - все, кроме nil:
  • 50.
    Таблицы в Lua Основныеопределения • Значения - все, кроме nil: local t = { 1, -- number 'test', -- string function() end, -- function {1, 2, 3} -- another table task:get_mempool(), -- userdata }
  • 51.
    Таблицы в Lua Основныеопределения • Значения - все, кроме nil: local t = { 1, -- number 'test', -- string function() end, -- function {1, 2, 3} -- another table task:get_mempool(), -- userdata } • Ключи - строки и числа:
  • 52.
    Таблицы в Lua Основныеопределения • Значения - все, кроме nil: local t = { 1, -- number 'test', -- string function() end, -- function {1, 2, 3} -- another table task:get_mempool(), -- userdata } local t = { 1, -- 1 [3] = {1,2}, test = function() end, ['spaces in key'] = 'abc', [1.2] = 3, [-1] = 4, } • Ключи - строки и числа:
  • 53.
    Таблицы в Lua table localtable = { a = 1, b = 2, [1] = 3, [2] = function() end, [0] = true } 3 function() end true 1 2 1 2 0 ‘a’ ‘b’ Массив Хеш таблица !
  • 54.
    Таблицы в Lua Итерация localt = { a = 1, b = 2, [1] = 3, [2] = function() end, [0] = true } Массив Таблица целиком 1..2
  • 55.
    Метатаблицы Когда просто таблицнедостаточно • Задают общие свойства для других таблиц • Примерно соответствуют Prototype в JavaScript • Позволяют задавать функции-методы для таблиц (через __newindex или __index) • Могут также задавать операторы над таблицами (например, через __eq или __sum) • Можно задавать метатаблицы для стандартных Lua типов (например, строк) • Используются в C API для определения методов для типа userdata
  • 56.
  • 57.
  • 58.
    Функции в Lua Основныеэлементы • Могут быть вложенными:
  • 59.
    Функции в Lua Основныеэлементы • Могут быть вложенными: function foo(n) -- global function local var = function(m) -- function in var return m + n -- n is from `foo` end local function bar() -- another form of var return var(2) end return bar() end
  • 60.
    Функции в Lua Основныеэлементы • Могут быть вложенными: function foo(n) -- global function local var = function(m) -- function in var return m + n -- n is from `foo` end local function bar() -- another form of var return var(2) end return bar() end • Могут быть аргументами и возвращаться из функций:
  • 61.
    Функции в Lua Основныеэлементы • Могут быть вложенными: function foo(n) -- global function local var = function(m) -- function in var return m + n -- n is from `foo` end local function bar() -- another form of var return var(2) end return bar() end local function f(cb) -- accepts callback function return function(args) -- accepts some args return cb(args) -- return callback function from args (decorator like) end end • Могут быть аргументами и возвращаться из функций:
  • 62.
    Функции в Lua Замыкания functiontest(n) local f = function() return n + 1 -- n is captured end n = n + 1 -- n in f is also modified return а end test(1) -- Returns function closure test(1)() -- Returns 3
  • 63.
    Функции в Lua Замыкания functiontest(n) local f = function() return n + 1 -- n is captured end n = n + 1 -- n in f is also modified return а end test(1) -- Returns function closure test(1)() -- Returns 3 Замыкание
  • 64.
    Функции в Lua Замыкания functiontest(n) local f = function() return n + 1 -- n is captured end n = n + 1 -- n in f is also modified return а end test(1) -- Returns function closure test(1)() -- Returns 3 Замыкание
  • 65.
    Функции в Lua Замыкания functiontest(n) local f = function() return n + 1 -- n is captured end n = n + 1 -- n in f is also modified return а end test(1) -- Returns function closure test(1)() -- Returns 3 Замыкание Переменные входят по ссылке
  • 66.
    Функции в Lua Замыкания functiontest(n) local f = function() return n + 1 -- n is captured end n = n + 1 -- n in f is also modified return а end test(1) -- Returns function closure test(1)() -- Returns 3 Замыкание Переменные входят по ссылке Можно возвращать функцию
  • 67.
    Функции в Lua Замыкания functiontest(n) local f = function() return n + 1 -- n is captured end n = n + 1 -- n in f is also modified return а end test(1) -- Returns function closure test(1)() -- Returns 3 Замыкание Переменные входят по ссылке Функция-замыкание Можно возвращать функцию
  • 68.
    Функции в Lua Замыкания functiontest(n) local f = function() return n + 1 -- n is captured end n = n + 1 -- n in f is also modified return а end test(1) -- Returns function closure test(1)() -- Returns 3 Замыкание Переменные входят по ссылке Функция-замыкание Непосредственно вызов Можно возвращать функцию
  • 69.
    Функции в Lua •Позволяют писать в функциональном стиле (библиотека lua-fun) • Замыкания работают также для C функций • Передача по ссылке несколько необычна, но эффективна • Время жизни замыкания ассоциируется с переменной, в которой оно хранится
  • 70.
  • 71.
    Строки в Lua Передачастроки из C Глобальный хеш строк s1 s2 s3 lua_pushstring (L, "hello"); hello Копия Хеш hash(hello) Поиск
  • 72.
    Строки в Lua Передачастроки из C Глобальный хеш строк s1 s2 s3 lua_pushstring (L, "hello"); hello Вставка hello
  • 73.
    Строки в Lua •Строки в Lua неизменяемы • Сравнение строк - просто сравнение указателей: O(1) • Создание и передача строк - дорогая операция • Если нужно делать строку из кусков, то нужно использовать таблицу и table.concat
  • 74.
  • 75.
    Строки в Lua Передачатекста C C Userdata const char *s size_t len refcounter
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
    Виртуальный стек Вызов функции 1 2 3 4 -3 -2 -1 5 6 Внутренниепеременые number string 4 5 -2 -1 Аргументы Возвращаемые значения
  • 81.
  • 82.
    Проблемы работы состеком • Скорость: переключение занимает около сотни CPU cycles
  • 83.
    Проблемы работы состеком • Скорость: переключение занимает около сотни CPU cycles • Проблемы контроля: без специальных ключей компиляции легко получить низкоуровневые ошибки (вплоть до полного падения)
  • 84.
    Проблемы работы состеком • Скорость: переключение занимает около сотни CPU cycles • Проблемы контроля: без специальных ключей компиляции легко получить низкоуровневые ошибки (вплоть до полного падения)
  • 85.
    Проблемы работы состеком • Скорость: переключение занимает около сотни CPU cycles • Проблемы контроля: без специальных ключей компиляции легко получить низкоуровневые ошибки (вплоть до полного падения) • Крайне неочевидный код
  • 86.
    Проблемы работы состеком • Скорость: переключение занимает около сотни CPU cycles • Проблемы контроля: без специальных ключей компиляции легко получить низкоуровневые ошибки (вплоть до полного падения) • Крайне неочевидный код • Есть ряд сложных моментов (итерация по таблице)
  • 87.
    Пример из практики Итерацияпо таблице lua_pushvalue (L, i); /* Push table on top */ lua_pushnil (L); /* Push nil to start iterate */ while (lua_next (L, -2)) { lua_pushvalue (L, -2); /* Copy key as it is special */ key = luaL_checkstring (L, -1); value = luaL_checkstring (L, -2); lua_pop (L, 2); /* Remove key and value leaving original key */ } lua_pop (L, 1); /* Remove table */
  • 88.
    Пример из практики Итерацияпо таблице lua_pushvalue (L, i); tabletop -1
  • 89.
    Пример из практики Итерацияпо таблице lua_pushvalue (L, i); lua_pushnil (L); tabletop -2 nil -1
  • 90.
    Пример из практики Итерацияпо таблице table -2 nil -1 while (lua_next (L, -2)) table -3 key -2 value -1 Предыдущий ключ Новый ключ
  • 91.
    Пример из практики Итерацияпо таблице lua_pushvalue (L, -2); key = luaL_checkstring (L, -1); value = luaL_checkstring (L, -2); table -2 key -1 value key copy -3 -4
  • 92.
    Пример из практики Итерацияпо таблице lua_pop (L, 2); table -2 key -1 value key copy -3 -4 table key -1 -2 while (lua_next (L, -2)) Используется для следующей итерации
  • 93.
  • 94.
    Плюсы FFI • Оченьбыстро работают для простых типов (десяток циклов) • Поддерживают все конструкции C99 • Поддерживают типы-обертки (boxed types), например 64-х битные целые • Проще поддерживать
  • 95.
  • 96.
    Пример из практики Оптимизацияузких мест local ffi if type(jit) == 'table' then ffi = require("ffi") ffi.cdef[[ int rspamd_re_cache_type_from_string (const char *str); int rspamd_re_cache_process_ffi (void *ptask, void *pre, int type, const char *type_data, int is_strong); ]] end local function process_regexp_opt(re, task, re_type, header, strong) if type(jit) == 'table' then -- Use ffi call local itype = ffi.C.rspamd_re_cache_type_from_string(re_type) if not strong then strong = 0 else string = 1 end local iret = ffi.C.rspamd_re_cache_process_ffi (task, re, itype, header, strong) return tonumber(iret) else return task:process_regexp(re, re_type, header, strong) end end Проверка LuaJIT
  • 97.
    local ffi if type(jit)== 'table' then ffi = require("ffi") ffi.cdef[[ int rspamd_re_cache_type_from_string (const char *str); int rspamd_re_cache_process_ffi (void *ptask, void *pre, int type, const char *type_data, int is_strong); ]] end local function process_regexp_opt(re, task, re_type, header, strong) if type(jit) == 'table' then -- Use ffi call local itype = ffi.C.rspamd_re_cache_type_from_string(re_type) if not strong then strong = 0 else string = 1 end local iret = ffi.C.rspamd_re_cache_process_ffi (task, re, itype, header, strong) return tonumber(iret) else return task:process_regexp(re, re_type, header, strong) end end Определение С функций Пример из практики Оптимизация узких мест
  • 98.
    local ffi if type(jit)== 'table' then ffi = require("ffi") ffi.cdef[[ int rspamd_re_cache_type_from_string (const char *str); int rspamd_re_cache_process_ffi (void *ptask, void *pre, int type, const char *type_data, int is_strong); ]] end local function process_regexp_opt(re, task, re_type, header, strong) if type(jit) == 'table' then -- Use ffi call local itype = ffi.C.rspamd_re_cache_type_from_string(re_type) if not strong then strong = 0 else string = 1 end local iret = ffi.C.rspamd_re_cache_process_ffi (task, re, itype, header, strong) return tonumber(iret) else return task:process_regexp(re, re_type, header, strong) end end FFI call Userdata to C pointer bool -> int Пример из практики Оптимизация узких мест
  • 99.
    local ffi if type(jit)== 'table' then ffi = require("ffi") ffi.cdef[[ int rspamd_re_cache_type_from_string (const char *str); int rspamd_re_cache_process_ffi (void *ptask, void *pre, int type, const char *type_data, int is_strong); ]] end local function process_regexp_opt(re, task, re_type, header, strong) if type(jit) == 'table' then -- Use ffi call local itype = ffi.C.rspamd_re_cache_type_from_string(re_type) if not strong then strong = 0 else string = 1 end local iret = ffi.C.rspamd_re_cache_process_ffi (task, re, itype, header, strong) return tonumber(iret) else return task:process_regexp(re, re_type, header, strong) end end Plain call Пример из практики Оптимизация узких мест
  • 100.
  • 101.
  • 102.
    Проблемы FFI • Поддерживаютсяна ограниченном количестве архитектур (Sparc64)
  • 103.
    Проблемы FFI • Поддерживаютсяна ограниченном количестве архитектур (Sparc64) • В plain Lua требуют отдельного модуля FFI, LuaJIT поддерживает “из коробки”
  • 104.
    Проблемы FFI • Поддерживаютсяна ограниченном количестве архитектур (Sparc64) • В plain Lua требуют отдельного модуля FFI, LuaJIT поддерживает “из коробки” • Нет проверки типов, нет контроля целостности памяти
  • 105.
    Проблемы FFI • Поддерживаютсяна ограниченном количестве архитектур (Sparc64) • В plain Lua требуют отдельного модуля FFI, LuaJIT поддерживает “из коробки” • Нет проверки типов, нет контроля целостности памяти
  • 106.
    Проблемы FFI • Поддерживаютсяна ограниченном количестве архитектур (Sparc64) • В plain Lua требуют отдельного модуля FFI, LuaJIT поддерживает “из коробки” • Нет проверки типов, нет контроля целостности памяти
  • 107.
    Проблемы FFI • Поддерживаютсяна ограниченном количестве архитектур (Sparc64) • В plain Lua требуют отдельного модуля FFI, LuaJIT поддерживает “из коробки” • Нет проверки типов, нет контроля целостности памяти • Не всегда быстрее
  • 108.
    Проблемы FFI • Поддерживаютсяна ограниченном количестве архитектур (Sparc64) • В plain Lua требуют отдельного модуля FFI, LuaJIT поддерживает “из коробки” • Нет проверки типов, нет контроля целостности памяти • Не всегда быстрее • Очень сложно сделать sandbox
  • 109.
  • 110.
  • 111.
    Выводы • Lua простойдля изучения
  • 112.
    Выводы • Lua простойдля изучения
  • 113.
    Выводы • Lua простойдля изучения • Lua удобен для встраивания
  • 114.
    Выводы • Lua простойдля изучения • Lua удобен для встраивания
  • 115.
  • 116.
  • 117.
  • 118.
    Выводы • Основной инструмент- таблицы • Основной инструмент - функции
  • 119.
    Выводы • Основной инструмент- таблицы • Основной инструмент - функции
  • 120.
  • 121.
    Выводы • Lua -быстрый язык (особенно LuaJIT)
  • 122.
    Выводы • Lua -быстрый язык (особенно LuaJIT)
  • 123.
    Выводы • Lua -быстрый язык (особенно LuaJIT) • Некоторые оптимизации бывают опасны
  • 124.
    Выводы • Lua -быстрый язык (особенно LuaJIT) • Некоторые оптимизации бывают опасны
  • 125.