Метрики LuaJIT
Tarantool может возвращать метрики текущего экземпляра через Lua API или C API.
- misc.getmetrics()
- Значения в таблице getmetrics
- C API для getmetrics
- Пример с gc_strnum, strhash_miss и strhash_hit
- Пример с gc_allocated и gc_freed
- Пример с gc_allocated и оптимизацией по памяти
- gc_steps_atomic и gc_steps_propagate
- Пример с jit_trace_num и jit_trace_abort
- Пример с jit_snap_restore и деоптимизацией производительности
misc.getmetrics()
-
getmetrics
()¶
Получение значений метрик в виде таблицы.
Параметры: нет
возвращает: таблицу
Пример: metrics_table = misc.getmetrics()
Значения в таблице getmetrics
Таблица метрик содержит 19 значений типа number
. Они получены в результате приведения к вещественному типу двойной точности (double), что позволяет практически не терять в точности исходных значений. Значения, имена которых начинаются на gc_
, связаны со сборщиком мусора в LuaJIT. Полную информацию о сборщике мусора можно найти на вики-странице lua-users и слайде от создателя языка Lua. Значения, имена которых начинаются на jit_
, связаны с фазами JIT-компиляции. Более подробно фазы описаны в научной работе, написанной в рамках одного из проектов ЦЕРН.
Монотонными называются суммарные значения, которые подсчитываются с момента начала работы, а не с момента последнего вызова getmetrics(). Возможно переполнение значений.
Поскольку многие значения монотонны, анализ обычно проходит так: вызывается getmetrics()
, полученная таблица сохраняется, а затем getmetrics()
вызывается вновь и новая таблица сравнивается с сохраненной. Разницу между значениями, измеренными в два момента времени, называют кривой наклона. Интерес может представлять, например, кривая наклона, которая демонстрирует ускорение. Разница между последним и предыдущим значениями на такой кривой все время увеличивается. Для некоторых элементов таблицы ниже будут приведены примеры.
Имя
Содержимое
Монотонное?
gc_allocated
количество выделенной памяти в байтах
да
gc_cdatanum
количество размещенных объектов cdata
нет
gc_freed
количество освобожденной памяти в байтах
да
gc_steps_atomic
количество шагов сборщика мусора, фаза atomic, инкрементальная
да
gc_steps_finalize
количество шагов сборщика мусора, фаза finalize
да
gc_steps_pause
количество шагов сборщика мусора, фаза pauses
да
gc_steps_propagate
количество шагов сборщика мусора, фаза propagate
да
gc_steps_sweep
number of steps of garbage collector,
sweep phases
(see the Sweep phase description)
да
gc_steps_sweepstring
количество шагов сборщика мусора, фаза sweep для строк
да
gc_strnum
количество размещенных объектов-строк
нет
gc_tabnum
количество размещенных объектов-таблиц
нет
gc_total
текущее количество выделенной памяти в байтах (обычно равно разности gc_allocated и gc_freed)
нет
gc_udatanum
количество размещенных объектов udata
нет
jit_mcode_size
общий объем выделенного машинного кода
нет
jit_snap_restore
общее количество восстановлений стека по снимку (сработавших защитных утверждений, которые привели к остановке выполнения трассы). См. внешнее руководство по SNAP.
да
jit_trace_abort
общее количество прерванных трассировок
да
jit_trace_num
количество JIT-трассировок
нет
strhash_hit
количество интернированных строк (если строка уже есть в пуле, новая копия не создается и память под нее не выделяется)
да
strhash_miss
общее количество памяти, выделенной для строк за время жизни платформы
да
Примечание. Функция ujit.getmetrics() возвращает похожие имена. Однако многие значения, используемые в uJIT, не монотонны.
Note: Although value names are similar to value names in LuaJIT metrics,
and the values are exactly the same, misc.getmetrics() is slightly easier
because there is no need to ‘require’ the misc module.
C API для getmetrics
Lua-функция getmetrics()
— обертка для C-функции luaM_metrics()
.
Программы на C могут включать заголовок libmisclib.h
, куда входят следующие определения:
struct luam_Metrics { /* имена, описанные ранее для Lua */ }
LUAMISC_API void luaM_metrics(lua_State *L, struct luam_Metrics *metrics);
Имена элементов структуры luam_Metrics
совпадают с именами в таблице значений getmetrics для Lua. У всех элементов структуры luam_Metrics
тип данных — size_t
. Функция luaM_metrics()
заполняет структуру *metrics
метриками, относящимися к Lua-состоянию, которое связано с корутиной L
.
Пример с программой на языке C
В руководстве Tarantool по хранимым процедурам на языке C перейдите к примеру с файлом easy.c
. Удалите содержимое файла и вставьте следующий код:
#include "module.h"
#include <lmisclib.h>
int easy(box_function_ctx_t *ctx, const char *args, const char *args_end)
{
lua_State *ls = luaT_state();
struct luam_Metrics m;
luaM_metrics(ls, &m);
printf("allocated memory = %lu\n", m.gc_allocated);
return 0;
}
Теперь, как в исходном примере, выполните через клиент запросы до capi_connection:call('easy')
включительно. На экране появится следующее: allocated memory = 4431950
(число приведено для примера).
Пример с gc_strnum, strhash_miss и strhash_hit
Отслеживать размещение новых строковых объектов можно так:
function f()
collectgarbage("collect")
local oldm = misc.getmetrics()
local table_of_strings = {}
for i = 3000, 4000 do table.insert(table_of_strings, tostring(i)) end
for i = 3900, 4100 do table.insert(table_of_strings, tostring(i)) end
local newm = misc.getmetrics()
print("gc_strnum diff = " .. newm.gc_strnum - oldm.gc_strnum)
print("strhash_miss diff = " .. newm.strhash_miss - oldm.strhash_miss)
print("strhash_hit diff = " .. newm.strhash_hit - oldm.strhash_hit)
end
f()
Вероятный результат — gc_strnum diff = 1101
, так как мы добавили 1202 строки, 101 из которых были дубликатами. По той же причине strhash_miss = 1101
, а strhash_hit = 101
плюс некоторые издержки. (strhash_hit
всегда предполагает небольшие издержки, которые можно игнорировать.)
Результат лишь вероятный, поскольку память для строк могла быть выделена ранее. Хорошо, если кривая наклона strhash_miss
менее крутая, чем у strhash_hit
.
Доступ к остальным значениям gc_*num
— gc_cdatanum
, gc_tabnum
и gc_udatanum
можно получить аналогичным образом. Любое значение gc_*num
поможет при поиске утечки памяти: общее количество этих объектов не должно постоянно расти. Более общий способ искать утечки памяти — наблюдать за переменной gc_total
. Также можно отслеживать значение jit_mcode_size
, отражающее объем памяти, выделенной для трасс машинного кода.
Пример с gc_allocated и gc_freed
Чем меньше работает сборщик мусора, тем лучше. Отслеживать, насколько его нагружает приложение, можно так:
function f()
for i = 1, 10 do collectgarbage("collect") end
local oldm = misc.getmetrics()
local newm = misc.getmetrics()
oldm = misc.getmetrics()
collectgarbage("collect")
newm = misc.getmetrics()
print("gc_allocated diff = " .. newm.gc_allocated - oldm.gc_allocated)
print("gc_freed diff = " .. newm.gc_freed - oldm.gc_freed)
end
f()
Результат: gc_allocated diff = 800
, gc_freed diff = 800
. Отсюда видно, что строка local ... = getmetrics()
вызывает аллокацию памяти, поскольку создает таблицу и наполняет ее значениями. Когда имя переменной (в данном случае oldm
) используется повторно, память освобождается. Обычно это происходит не сразу, однако collectgarbage("collect")
принудительно запускает сборку мусора, благодаря чему можно немедленно увидеть результат.
Пример с gc_allocated и оптимизацией по памяти
Этот пример показывает, что можно оптимизировать расход памяти для таблиц.
function f()
collectgarbage("collect")
local oldm = misc.getmetrics()
local t = {}
for i = 1, 513 do
t[i] = i
end
local newm = misc.getmetrics()
local diff = newm.gc_allocated - oldm.gc_allocated
print("diff = " .. diff)
end
f()
Результат покажет, что значение diff примерно равно 18000.
Если инициализировать таблицу по-другому, получится вот что:
function f()
local table_new = require "table.new"
local oldm = misc.getmetrics()
local t = table_new(513, 0)
for i = 1, 513 do
t[i] = i
end
local newm = misc.getmetrics()
local diff = newm.gc_allocated - oldm.gc_allocated
print("diff = " .. diff)
end
f()
Результат покажет, что значение diff примерно равно 6000.
gc_steps_atomic и gc_steps_propagate
Нагрузку на сборщик мусора помогают отслеживать кривые наклона метрик gc_steps_*
. Во время долго выполняющихся процедур значения gc_steps_*
увеличиваются. При этом большие промежутки времени между увеличениями gc_steps_atomic
— хороший признак. Поскольку gc_steps_atomic
увеличивается только раз в цикл сбора мусора, можно увидеть, сколько циклов прошло.
Кроме того, по тому, насколько выросло значение gc_steps_propagate
, можно косвенно оценить количество объектов. Это значение также коррелирует с множителем шага сборщика мусора. Например, количество инкрементальных шагов может увеличиваться, однако множитель шага настроен так, что за каждый шаг можно обработать лишь небольшое количество объектов. Поэтому при настройке сборщика мусора следует учитывать эти показатели.
Следующая функция оценивает, вызывает ли оператор SQL большую нагрузку:
function f()
collectgarbage("collect")
local oldm = misc.getmetrics()
collectgarbage("collect")
box.execute([[DROP TABLE _vindex;]])
local newm = misc.getmetrics()
print("gc_steps_atomic = " .. newm.gc_steps_atomic - oldm.gc_steps_atomic)
print("gc_steps_finalize = " .. newm.gc_steps_finalize - oldm.gc_steps_finalize)
print("gc_steps_pause = " .. newm.gc_steps_pause - oldm.gc_steps_pause)
print("gc_steps_propagate = " .. newm.gc_steps_propagate - oldm.gc_steps_propagate)
print("gc_steps_sweep = " .. newm.gc_steps_sweep - oldm.gc_steps_sweep)
end
f()
Очевидно, значения метрик gc_steps_ *
, полученные до вызова box.execute()
, существенно не отличаются от значений, полученных после этого вызова.
Пример с jit_trace_num и jit_trace_abort
JIT-компиляторы трассируют код, пытаясь найти возможность для компиляции, чтобы повысить производительность. Значение jit_trace_abort
показывает, как часто эти попытки оканчиваются неудачей (чем меньше это значение, тем лучше), а jit_trace_num
— как много трассировок сгенерировано с момента последнего выполнения операции flush (обычно чем больше значение, тем лучше).
Код следующей функции будет успешно трассирован:
function f()
jit.flush()
for i = 1, 10 do collectgarbage("collect") end
local oldm = misc.getmetrics()
collectgarbage("collect")
local sum = 0
for i = 1, 57 do
sum = sum + 57
end
for i = 1, 10 do collectgarbage("collect") end
local newm = misc.getmetrics()
print("trace_num = " .. newm.jit_trace_num - oldm.jit_trace_num)
print("trace_abort = " .. newm.jit_trace_abort - oldm.jit_trace_abort)
end
f()
Результат: trace_num = 1
, trace_abort = 0
. Отлично.
А в следующей функции, похоже, есть код, способный вызвать у LuaJIT проблемы:
jit.opt.start(0, "hotloop=2", "hotexit=2", "minstitch=15")
_G.globalthing = 5
function f()
jit.flush()
collectgarbage("collect")
local oldm = misc.getmetrics()
collectgarbage("collect")
local sum = 0
for i = 1, box.space._vindex:count()+ _G.globalthing do
box.execute([[SELECT RANDOMBLOB(0);]])
require('buffer').ibuf()
_G.globalthing = _G.globalthing - 1
end
local newm = misc.getmetrics()
print("trace_num = " .. newm.jit_trace_num - oldm.jit_trace_num)
print("trace_abort = " .. newm.jit_trace_abort - oldm.jit_trace_abort)
end
f()
Результат: trace_num
— от 2
до 4
, trace_abort = 1
. Это означает, что нужно было сгенерировать до четырех трассировок вместо одной, причем что-то заставило LuaJIT прекратить попытки. Дальнейшая трассировка показывает, что проблема не в подозрительно выглядящих операторах внутри функции, а в вызове jit.opt.start
. (Содержимое файла jit.dump
может помочь разобраться, как протекала компиляция трасс.)
Пример с jit_snap_restore и деоптимизацией производительности
Если кривая наклона метрики jit_snap_restore
после изменений в старом коде растет, это может означать, что LuaJIT чаще останавливает выполнение кода на трассах. А это, в свою очередь, может указывать на снижение производительности.
Рассмотрим следующий код:
function f()
local function foo(i)
return i <= 5 and i or tostring(i)
end
-- параметр minstitch нужен, чтобы эмулировать поведение non-stitching
jit.opt.start(0, "hotloop=2", "hotexit=2", "minstitch=15")
local sum = 0
local oldm = misc.getmetrics()
for i = 1, 10 do
sum = sum + foo(i)
end
local newm = misc.getmetrics()
local diff = newm.jit_snap_restore - oldm.jit_snap_restore
print("diff = " .. diff)
end
f()
Результат: diff = 3
, поскольку при выполнении трассы возможны три сторонних выхода. Один находится в конце цикла. Другие два переключают выполнение на интерпретатор, прежде чем LuaJIT решит, что фрагмент кода «горячий» (значение параметра hotloop
по умолчанию равно 56
согласно документации LuaJIT).
А теперь изменим единственную строку в функции local foo
, чтобы получить следующий код:
function f()
local function foo(i)
-- функция math.fmod еще не скомпилирована
return i <= 5 and i or math.fmod(i, 11)
end
-- параметр minstitch нужен, чтобы эмулировать поведение non-stitching
jit.opt.start(0, "hotloop=2", "hotexit=2", "minstitch=15")
local sum = 0
local oldm = misc.getmetrics()
for i = 1, 10 do
sum = sum + foo(i)
end
local newm = misc.getmetrics()
local diff = newm.jit_snap_restore - oldm.jit_snap_restore
print("diff = " .. diff)
end
f()
Результат: значение diff
увеличилось, так как сторонних выходов стало больше. Этот тест показывает, что измение кода влияет на производительность.
-
getmetrics
()¶ Получение значений метрик в виде таблицы.
Параметры: нет
возвращает: таблицу Пример:
metrics_table = misc.getmetrics()
Значения в таблице getmetrics
Таблица метрик содержит 19 значений типа number
. Они получены в результате приведения к вещественному типу двойной точности (double), что позволяет практически не терять в точности исходных значений. Значения, имена которых начинаются на gc_
, связаны со сборщиком мусора в LuaJIT. Полную информацию о сборщике мусора можно найти на вики-странице lua-users и слайде от создателя языка Lua. Значения, имена которых начинаются на jit_
, связаны с фазами JIT-компиляции. Более подробно фазы описаны в научной работе, написанной в рамках одного из проектов ЦЕРН.
Монотонными называются суммарные значения, которые подсчитываются с момента начала работы, а не с момента последнего вызова getmetrics(). Возможно переполнение значений.
Поскольку многие значения монотонны, анализ обычно проходит так: вызывается getmetrics()
, полученная таблица сохраняется, а затем getmetrics()
вызывается вновь и новая таблица сравнивается с сохраненной. Разницу между значениями, измеренными в два момента времени, называют кривой наклона. Интерес может представлять, например, кривая наклона, которая демонстрирует ускорение. Разница между последним и предыдущим значениями на такой кривой все время увеличивается. Для некоторых элементов таблицы ниже будут приведены примеры.
Имя
Содержимое
Монотонное?
gc_allocated
количество выделенной памяти в байтах
да
gc_cdatanum
количество размещенных объектов cdata
нет
gc_freed
количество освобожденной памяти в байтах
да
gc_steps_atomic
количество шагов сборщика мусора, фаза atomic, инкрементальная
да
gc_steps_finalize
количество шагов сборщика мусора, фаза finalize
да
gc_steps_pause
количество шагов сборщика мусора, фаза pauses
да
gc_steps_propagate
количество шагов сборщика мусора, фаза propagate
да
gc_steps_sweep
number of steps of garbage collector,
sweep phases
(see the Sweep phase description)
да
gc_steps_sweepstring
количество шагов сборщика мусора, фаза sweep для строк
да
gc_strnum
количество размещенных объектов-строк
нет
gc_tabnum
количество размещенных объектов-таблиц
нет
gc_total
текущее количество выделенной памяти в байтах (обычно равно разности gc_allocated и gc_freed)
нет
gc_udatanum
количество размещенных объектов udata
нет
jit_mcode_size
общий объем выделенного машинного кода
нет
jit_snap_restore
общее количество восстановлений стека по снимку (сработавших защитных утверждений, которые привели к остановке выполнения трассы). См. внешнее руководство по SNAP.
да
jit_trace_abort
общее количество прерванных трассировок
да
jit_trace_num
количество JIT-трассировок
нет
strhash_hit
количество интернированных строк (если строка уже есть в пуле, новая копия не создается и память под нее не выделяется)
да
strhash_miss
общее количество памяти, выделенной для строк за время жизни платформы
да
Примечание. Функция ujit.getmetrics() возвращает похожие имена. Однако многие значения, используемые в uJIT, не монотонны.
Note: Although value names are similar to value names in LuaJIT metrics,
and the values are exactly the same, misc.getmetrics() is slightly easier
because there is no need to ‘require’ the misc module.
C API для getmetrics
Lua-функция getmetrics()
— обертка для C-функции luaM_metrics()
.
Программы на C могут включать заголовок libmisclib.h
, куда входят следующие определения:
struct luam_Metrics { /* имена, описанные ранее для Lua */ }
LUAMISC_API void luaM_metrics(lua_State *L, struct luam_Metrics *metrics);
Имена элементов структуры luam_Metrics
совпадают с именами в таблице значений getmetrics для Lua. У всех элементов структуры luam_Metrics
тип данных — size_t
. Функция luaM_metrics()
заполняет структуру *metrics
метриками, относящимися к Lua-состоянию, которое связано с корутиной L
.
Пример с программой на языке C
В руководстве Tarantool по хранимым процедурам на языке C перейдите к примеру с файлом easy.c
. Удалите содержимое файла и вставьте следующий код:
#include "module.h"
#include <lmisclib.h>
int easy(box_function_ctx_t *ctx, const char *args, const char *args_end)
{
lua_State *ls = luaT_state();
struct luam_Metrics m;
luaM_metrics(ls, &m);
printf("allocated memory = %lu\n", m.gc_allocated);
return 0;
}
Теперь, как в исходном примере, выполните через клиент запросы до capi_connection:call('easy')
включительно. На экране появится следующее: allocated memory = 4431950
(число приведено для примера).
Пример с gc_strnum, strhash_miss и strhash_hit
Отслеживать размещение новых строковых объектов можно так:
function f()
collectgarbage("collect")
local oldm = misc.getmetrics()
local table_of_strings = {}
for i = 3000, 4000 do table.insert(table_of_strings, tostring(i)) end
for i = 3900, 4100 do table.insert(table_of_strings, tostring(i)) end
local newm = misc.getmetrics()
print("gc_strnum diff = " .. newm.gc_strnum - oldm.gc_strnum)
print("strhash_miss diff = " .. newm.strhash_miss - oldm.strhash_miss)
print("strhash_hit diff = " .. newm.strhash_hit - oldm.strhash_hit)
end
f()
Вероятный результат — gc_strnum diff = 1101
, так как мы добавили 1202 строки, 101 из которых были дубликатами. По той же причине strhash_miss = 1101
, а strhash_hit = 101
плюс некоторые издержки. (strhash_hit
всегда предполагает небольшие издержки, которые можно игнорировать.)
Результат лишь вероятный, поскольку память для строк могла быть выделена ранее. Хорошо, если кривая наклона strhash_miss
менее крутая, чем у strhash_hit
.
Доступ к остальным значениям gc_*num
— gc_cdatanum
, gc_tabnum
и gc_udatanum
можно получить аналогичным образом. Любое значение gc_*num
поможет при поиске утечки памяти: общее количество этих объектов не должно постоянно расти. Более общий способ искать утечки памяти — наблюдать за переменной gc_total
. Также можно отслеживать значение jit_mcode_size
, отражающее объем памяти, выделенной для трасс машинного кода.
Пример с gc_allocated и gc_freed
Чем меньше работает сборщик мусора, тем лучше. Отслеживать, насколько его нагружает приложение, можно так:
function f()
for i = 1, 10 do collectgarbage("collect") end
local oldm = misc.getmetrics()
local newm = misc.getmetrics()
oldm = misc.getmetrics()
collectgarbage("collect")
newm = misc.getmetrics()
print("gc_allocated diff = " .. newm.gc_allocated - oldm.gc_allocated)
print("gc_freed diff = " .. newm.gc_freed - oldm.gc_freed)
end
f()
Результат: gc_allocated diff = 800
, gc_freed diff = 800
. Отсюда видно, что строка local ... = getmetrics()
вызывает аллокацию памяти, поскольку создает таблицу и наполняет ее значениями. Когда имя переменной (в данном случае oldm
) используется повторно, память освобождается. Обычно это происходит не сразу, однако collectgarbage("collect")
принудительно запускает сборку мусора, благодаря чему можно немедленно увидеть результат.
Пример с gc_allocated и оптимизацией по памяти
Этот пример показывает, что можно оптимизировать расход памяти для таблиц.
function f()
collectgarbage("collect")
local oldm = misc.getmetrics()
local t = {}
for i = 1, 513 do
t[i] = i
end
local newm = misc.getmetrics()
local diff = newm.gc_allocated - oldm.gc_allocated
print("diff = " .. diff)
end
f()
Результат покажет, что значение diff примерно равно 18000.
Если инициализировать таблицу по-другому, получится вот что:
function f()
local table_new = require "table.new"
local oldm = misc.getmetrics()
local t = table_new(513, 0)
for i = 1, 513 do
t[i] = i
end
local newm = misc.getmetrics()
local diff = newm.gc_allocated - oldm.gc_allocated
print("diff = " .. diff)
end
f()
Результат покажет, что значение diff примерно равно 6000.
gc_steps_atomic и gc_steps_propagate
Нагрузку на сборщик мусора помогают отслеживать кривые наклона метрик gc_steps_*
. Во время долго выполняющихся процедур значения gc_steps_*
увеличиваются. При этом большие промежутки времени между увеличениями gc_steps_atomic
— хороший признак. Поскольку gc_steps_atomic
увеличивается только раз в цикл сбора мусора, можно увидеть, сколько циклов прошло.
Кроме того, по тому, насколько выросло значение gc_steps_propagate
, можно косвенно оценить количество объектов. Это значение также коррелирует с множителем шага сборщика мусора. Например, количество инкрементальных шагов может увеличиваться, однако множитель шага настроен так, что за каждый шаг можно обработать лишь небольшое количество объектов. Поэтому при настройке сборщика мусора следует учитывать эти показатели.
Следующая функция оценивает, вызывает ли оператор SQL большую нагрузку:
function f()
collectgarbage("collect")
local oldm = misc.getmetrics()
collectgarbage("collect")
box.execute([[DROP TABLE _vindex;]])
local newm = misc.getmetrics()
print("gc_steps_atomic = " .. newm.gc_steps_atomic - oldm.gc_steps_atomic)
print("gc_steps_finalize = " .. newm.gc_steps_finalize - oldm.gc_steps_finalize)
print("gc_steps_pause = " .. newm.gc_steps_pause - oldm.gc_steps_pause)
print("gc_steps_propagate = " .. newm.gc_steps_propagate - oldm.gc_steps_propagate)
print("gc_steps_sweep = " .. newm.gc_steps_sweep - oldm.gc_steps_sweep)
end
f()
Очевидно, значения метрик gc_steps_ *
, полученные до вызова box.execute()
, существенно не отличаются от значений, полученных после этого вызова.
Пример с jit_trace_num и jit_trace_abort
JIT-компиляторы трассируют код, пытаясь найти возможность для компиляции, чтобы повысить производительность. Значение jit_trace_abort
показывает, как часто эти попытки оканчиваются неудачей (чем меньше это значение, тем лучше), а jit_trace_num
— как много трассировок сгенерировано с момента последнего выполнения операции flush (обычно чем больше значение, тем лучше).
Код следующей функции будет успешно трассирован:
function f()
jit.flush()
for i = 1, 10 do collectgarbage("collect") end
local oldm = misc.getmetrics()
collectgarbage("collect")
local sum = 0
for i = 1, 57 do
sum = sum + 57
end
for i = 1, 10 do collectgarbage("collect") end
local newm = misc.getmetrics()
print("trace_num = " .. newm.jit_trace_num - oldm.jit_trace_num)
print("trace_abort = " .. newm.jit_trace_abort - oldm.jit_trace_abort)
end
f()
Результат: trace_num = 1
, trace_abort = 0
. Отлично.
А в следующей функции, похоже, есть код, способный вызвать у LuaJIT проблемы:
jit.opt.start(0, "hotloop=2", "hotexit=2", "minstitch=15")
_G.globalthing = 5
function f()
jit.flush()
collectgarbage("collect")
local oldm = misc.getmetrics()
collectgarbage("collect")
local sum = 0
for i = 1, box.space._vindex:count()+ _G.globalthing do
box.execute([[SELECT RANDOMBLOB(0);]])
require('buffer').ibuf()
_G.globalthing = _G.globalthing - 1
end
local newm = misc.getmetrics()
print("trace_num = " .. newm.jit_trace_num - oldm.jit_trace_num)
print("trace_abort = " .. newm.jit_trace_abort - oldm.jit_trace_abort)
end
f()
Результат: trace_num
— от 2
до 4
, trace_abort = 1
. Это означает, что нужно было сгенерировать до четырех трассировок вместо одной, причем что-то заставило LuaJIT прекратить попытки. Дальнейшая трассировка показывает, что проблема не в подозрительно выглядящих операторах внутри функции, а в вызове jit.opt.start
. (Содержимое файла jit.dump
может помочь разобраться, как протекала компиляция трасс.)
Пример с jit_snap_restore и деоптимизацией производительности
Если кривая наклона метрики jit_snap_restore
после изменений в старом коде растет, это может означать, что LuaJIT чаще останавливает выполнение кода на трассах. А это, в свою очередь, может указывать на снижение производительности.
Рассмотрим следующий код:
function f()
local function foo(i)
return i <= 5 and i or tostring(i)
end
-- параметр minstitch нужен, чтобы эмулировать поведение non-stitching
jit.opt.start(0, "hotloop=2", "hotexit=2", "minstitch=15")
local sum = 0
local oldm = misc.getmetrics()
for i = 1, 10 do
sum = sum + foo(i)
end
local newm = misc.getmetrics()
local diff = newm.jit_snap_restore - oldm.jit_snap_restore
print("diff = " .. diff)
end
f()
Результат: diff = 3
, поскольку при выполнении трассы возможны три сторонних выхода. Один находится в конце цикла. Другие два переключают выполнение на интерпретатор, прежде чем LuaJIT решит, что фрагмент кода «горячий» (значение параметра hotloop
по умолчанию равно 56
согласно документации LuaJIT).
А теперь изменим единственную строку в функции local foo
, чтобы получить следующий код:
function f()
local function foo(i)
-- функция math.fmod еще не скомпилирована
return i <= 5 and i or math.fmod(i, 11)
end
-- параметр minstitch нужен, чтобы эмулировать поведение non-stitching
jit.opt.start(0, "hotloop=2", "hotexit=2", "minstitch=15")
local sum = 0
local oldm = misc.getmetrics()
for i = 1, 10 do
sum = sum + foo(i)
end
local newm = misc.getmetrics()
local diff = newm.jit_snap_restore - oldm.jit_snap_restore
print("diff = " .. diff)
end
f()
Результат: значение diff
увеличилось, так как сторонних выходов стало больше. Этот тест показывает, что измение кода влияет на производительность.
Таблица метрик содержит 19 значений типа number
. Они получены в результате приведения к вещественному типу двойной точности (double), что позволяет практически не терять в точности исходных значений. Значения, имена которых начинаются на gc_
, связаны со сборщиком мусора в LuaJIT. Полную информацию о сборщике мусора можно найти на вики-странице lua-users и слайде от создателя языка Lua. Значения, имена которых начинаются на jit_
, связаны с фазами JIT-компиляции. Более подробно фазы описаны в научной работе, написанной в рамках одного из проектов ЦЕРН.
Монотонными называются суммарные значения, которые подсчитываются с момента начала работы, а не с момента последнего вызова getmetrics(). Возможно переполнение значений.
Поскольку многие значения монотонны, анализ обычно проходит так: вызывается getmetrics()
, полученная таблица сохраняется, а затем getmetrics()
вызывается вновь и новая таблица сравнивается с сохраненной. Разницу между значениями, измеренными в два момента времени, называют кривой наклона. Интерес может представлять, например, кривая наклона, которая демонстрирует ускорение. Разница между последним и предыдущим значениями на такой кривой все время увеличивается. Для некоторых элементов таблицы ниже будут приведены примеры.
Имя | Содержимое | Монотонное? |
---|---|---|
gc_allocated | количество выделенной памяти в байтах | да |
gc_cdatanum | количество размещенных объектов cdata | нет |
gc_freed | количество освобожденной памяти в байтах | да |
gc_steps_atomic | количество шагов сборщика мусора, фаза atomic, инкрементальная | да |
gc_steps_finalize | количество шагов сборщика мусора, фаза finalize | да |
gc_steps_pause | количество шагов сборщика мусора, фаза pauses | да |
gc_steps_propagate | количество шагов сборщика мусора, фаза propagate | да |
gc_steps_sweep | number of steps of garbage collector, sweep phases (see the Sweep phase description) | да |
gc_steps_sweepstring | количество шагов сборщика мусора, фаза sweep для строк | да |
gc_strnum | количество размещенных объектов-строк | нет |
gc_tabnum | количество размещенных объектов-таблиц | нет |
gc_total | текущее количество выделенной памяти в байтах (обычно равно разности gc_allocated и gc_freed) | нет |
gc_udatanum | количество размещенных объектов udata | нет |
jit_mcode_size | общий объем выделенного машинного кода | нет |
jit_snap_restore | общее количество восстановлений стека по снимку (сработавших защитных утверждений, которые привели к остановке выполнения трассы). См. внешнее руководство по SNAP. | да |
jit_trace_abort | общее количество прерванных трассировок | да |
jit_trace_num | количество JIT-трассировок | нет |
strhash_hit | количество интернированных строк (если строка уже есть в пуле, новая копия не создается и память под нее не выделяется) | да |
strhash_miss | общее количество памяти, выделенной для строк за время жизни платформы | да |
Примечание. Функция ujit.getmetrics() возвращает похожие имена. Однако многие значения, используемые в uJIT, не монотонны.
Note: Although value names are similar to value names in LuaJIT metrics, and the values are exactly the same, misc.getmetrics() is slightly easier because there is no need to ‘require’ the misc module.
C API для getmetrics
Lua-функция getmetrics()
— обертка для C-функции luaM_metrics()
.
Программы на C могут включать заголовок libmisclib.h
, куда входят следующие определения:
struct luam_Metrics { /* имена, описанные ранее для Lua */ }
LUAMISC_API void luaM_metrics(lua_State *L, struct luam_Metrics *metrics);
Имена элементов структуры luam_Metrics
совпадают с именами в таблице значений getmetrics для Lua. У всех элементов структуры luam_Metrics
тип данных — size_t
. Функция luaM_metrics()
заполняет структуру *metrics
метриками, относящимися к Lua-состоянию, которое связано с корутиной L
.
Пример с программой на языке C
В руководстве Tarantool по хранимым процедурам на языке C перейдите к примеру с файлом easy.c
. Удалите содержимое файла и вставьте следующий код:
#include "module.h"
#include <lmisclib.h>
int easy(box_function_ctx_t *ctx, const char *args, const char *args_end)
{
lua_State *ls = luaT_state();
struct luam_Metrics m;
luaM_metrics(ls, &m);
printf("allocated memory = %lu\n", m.gc_allocated);
return 0;
}
Теперь, как в исходном примере, выполните через клиент запросы до capi_connection:call('easy')
включительно. На экране появится следующее: allocated memory = 4431950
(число приведено для примера).
Пример с gc_strnum, strhash_miss и strhash_hit
Отслеживать размещение новых строковых объектов можно так:
function f()
collectgarbage("collect")
local oldm = misc.getmetrics()
local table_of_strings = {}
for i = 3000, 4000 do table.insert(table_of_strings, tostring(i)) end
for i = 3900, 4100 do table.insert(table_of_strings, tostring(i)) end
local newm = misc.getmetrics()
print("gc_strnum diff = " .. newm.gc_strnum - oldm.gc_strnum)
print("strhash_miss diff = " .. newm.strhash_miss - oldm.strhash_miss)
print("strhash_hit diff = " .. newm.strhash_hit - oldm.strhash_hit)
end
f()
Вероятный результат — gc_strnum diff = 1101
, так как мы добавили 1202 строки, 101 из которых были дубликатами. По той же причине strhash_miss = 1101
, а strhash_hit = 101
плюс некоторые издержки. (strhash_hit
всегда предполагает небольшие издержки, которые можно игнорировать.)
Результат лишь вероятный, поскольку память для строк могла быть выделена ранее. Хорошо, если кривая наклона strhash_miss
менее крутая, чем у strhash_hit
.
Доступ к остальным значениям gc_*num
— gc_cdatanum
, gc_tabnum
и gc_udatanum
можно получить аналогичным образом. Любое значение gc_*num
поможет при поиске утечки памяти: общее количество этих объектов не должно постоянно расти. Более общий способ искать утечки памяти — наблюдать за переменной gc_total
. Также можно отслеживать значение jit_mcode_size
, отражающее объем памяти, выделенной для трасс машинного кода.
Пример с gc_allocated и gc_freed
Чем меньше работает сборщик мусора, тем лучше. Отслеживать, насколько его нагружает приложение, можно так:
function f()
for i = 1, 10 do collectgarbage("collect") end
local oldm = misc.getmetrics()
local newm = misc.getmetrics()
oldm = misc.getmetrics()
collectgarbage("collect")
newm = misc.getmetrics()
print("gc_allocated diff = " .. newm.gc_allocated - oldm.gc_allocated)
print("gc_freed diff = " .. newm.gc_freed - oldm.gc_freed)
end
f()
Результат: gc_allocated diff = 800
, gc_freed diff = 800
. Отсюда видно, что строка local ... = getmetrics()
вызывает аллокацию памяти, поскольку создает таблицу и наполняет ее значениями. Когда имя переменной (в данном случае oldm
) используется повторно, память освобождается. Обычно это происходит не сразу, однако collectgarbage("collect")
принудительно запускает сборку мусора, благодаря чему можно немедленно увидеть результат.
Пример с gc_allocated и оптимизацией по памяти
Этот пример показывает, что можно оптимизировать расход памяти для таблиц.
function f()
collectgarbage("collect")
local oldm = misc.getmetrics()
local t = {}
for i = 1, 513 do
t[i] = i
end
local newm = misc.getmetrics()
local diff = newm.gc_allocated - oldm.gc_allocated
print("diff = " .. diff)
end
f()
Результат покажет, что значение diff примерно равно 18000.
Если инициализировать таблицу по-другому, получится вот что:
function f()
local table_new = require "table.new"
local oldm = misc.getmetrics()
local t = table_new(513, 0)
for i = 1, 513 do
t[i] = i
end
local newm = misc.getmetrics()
local diff = newm.gc_allocated - oldm.gc_allocated
print("diff = " .. diff)
end
f()
Результат покажет, что значение diff примерно равно 6000.
gc_steps_atomic и gc_steps_propagate
Нагрузку на сборщик мусора помогают отслеживать кривые наклона метрик gc_steps_*
. Во время долго выполняющихся процедур значения gc_steps_*
увеличиваются. При этом большие промежутки времени между увеличениями gc_steps_atomic
— хороший признак. Поскольку gc_steps_atomic
увеличивается только раз в цикл сбора мусора, можно увидеть, сколько циклов прошло.
Кроме того, по тому, насколько выросло значение gc_steps_propagate
, можно косвенно оценить количество объектов. Это значение также коррелирует с множителем шага сборщика мусора. Например, количество инкрементальных шагов может увеличиваться, однако множитель шага настроен так, что за каждый шаг можно обработать лишь небольшое количество объектов. Поэтому при настройке сборщика мусора следует учитывать эти показатели.
Следующая функция оценивает, вызывает ли оператор SQL большую нагрузку:
function f()
collectgarbage("collect")
local oldm = misc.getmetrics()
collectgarbage("collect")
box.execute([[DROP TABLE _vindex;]])
local newm = misc.getmetrics()
print("gc_steps_atomic = " .. newm.gc_steps_atomic - oldm.gc_steps_atomic)
print("gc_steps_finalize = " .. newm.gc_steps_finalize - oldm.gc_steps_finalize)
print("gc_steps_pause = " .. newm.gc_steps_pause - oldm.gc_steps_pause)
print("gc_steps_propagate = " .. newm.gc_steps_propagate - oldm.gc_steps_propagate)
print("gc_steps_sweep = " .. newm.gc_steps_sweep - oldm.gc_steps_sweep)
end
f()
Очевидно, значения метрик gc_steps_ *
, полученные до вызова box.execute()
, существенно не отличаются от значений, полученных после этого вызова.
Пример с jit_trace_num и jit_trace_abort
JIT-компиляторы трассируют код, пытаясь найти возможность для компиляции, чтобы повысить производительность. Значение jit_trace_abort
показывает, как часто эти попытки оканчиваются неудачей (чем меньше это значение, тем лучше), а jit_trace_num
— как много трассировок сгенерировано с момента последнего выполнения операции flush (обычно чем больше значение, тем лучше).
Код следующей функции будет успешно трассирован:
function f()
jit.flush()
for i = 1, 10 do collectgarbage("collect") end
local oldm = misc.getmetrics()
collectgarbage("collect")
local sum = 0
for i = 1, 57 do
sum = sum + 57
end
for i = 1, 10 do collectgarbage("collect") end
local newm = misc.getmetrics()
print("trace_num = " .. newm.jit_trace_num - oldm.jit_trace_num)
print("trace_abort = " .. newm.jit_trace_abort - oldm.jit_trace_abort)
end
f()
Результат: trace_num = 1
, trace_abort = 0
. Отлично.
А в следующей функции, похоже, есть код, способный вызвать у LuaJIT проблемы:
jit.opt.start(0, "hotloop=2", "hotexit=2", "minstitch=15")
_G.globalthing = 5
function f()
jit.flush()
collectgarbage("collect")
local oldm = misc.getmetrics()
collectgarbage("collect")
local sum = 0
for i = 1, box.space._vindex:count()+ _G.globalthing do
box.execute([[SELECT RANDOMBLOB(0);]])
require('buffer').ibuf()
_G.globalthing = _G.globalthing - 1
end
local newm = misc.getmetrics()
print("trace_num = " .. newm.jit_trace_num - oldm.jit_trace_num)
print("trace_abort = " .. newm.jit_trace_abort - oldm.jit_trace_abort)
end
f()
Результат: trace_num
— от 2
до 4
, trace_abort = 1
. Это означает, что нужно было сгенерировать до четырех трассировок вместо одной, причем что-то заставило LuaJIT прекратить попытки. Дальнейшая трассировка показывает, что проблема не в подозрительно выглядящих операторах внутри функции, а в вызове jit.opt.start
. (Содержимое файла jit.dump
может помочь разобраться, как протекала компиляция трасс.)
Пример с jit_snap_restore и деоптимизацией производительности
Если кривая наклона метрики jit_snap_restore
после изменений в старом коде растет, это может означать, что LuaJIT чаще останавливает выполнение кода на трассах. А это, в свою очередь, может указывать на снижение производительности.
Рассмотрим следующий код:
function f()
local function foo(i)
return i <= 5 and i or tostring(i)
end
-- параметр minstitch нужен, чтобы эмулировать поведение non-stitching
jit.opt.start(0, "hotloop=2", "hotexit=2", "minstitch=15")
local sum = 0
local oldm = misc.getmetrics()
for i = 1, 10 do
sum = sum + foo(i)
end
local newm = misc.getmetrics()
local diff = newm.jit_snap_restore - oldm.jit_snap_restore
print("diff = " .. diff)
end
f()
Результат: diff = 3
, поскольку при выполнении трассы возможны три сторонних выхода. Один находится в конце цикла. Другие два переключают выполнение на интерпретатор, прежде чем LuaJIT решит, что фрагмент кода «горячий» (значение параметра hotloop
по умолчанию равно 56
согласно документации LuaJIT).
А теперь изменим единственную строку в функции local foo
, чтобы получить следующий код:
function f()
local function foo(i)
-- функция math.fmod еще не скомпилирована
return i <= 5 and i or math.fmod(i, 11)
end
-- параметр minstitch нужен, чтобы эмулировать поведение non-stitching
jit.opt.start(0, "hotloop=2", "hotexit=2", "minstitch=15")
local sum = 0
local oldm = misc.getmetrics()
for i = 1, 10 do
sum = sum + foo(i)
end
local newm = misc.getmetrics()
local diff = newm.jit_snap_restore - oldm.jit_snap_restore
print("diff = " .. diff)
end
f()
Результат: значение diff
увеличилось, так как сторонних выходов стало больше. Этот тест показывает, что измение кода влияет на производительность.
Lua-функция getmetrics()
— обертка для C-функции luaM_metrics()
.
Программы на C могут включать заголовок libmisclib.h
, куда входят следующие определения:
struct luam_Metrics { /* имена, описанные ранее для Lua */ }
LUAMISC_API void luaM_metrics(lua_State *L, struct luam_Metrics *metrics);
Имена элементов структуры luam_Metrics
совпадают с именами в таблице значений getmetrics для Lua. У всех элементов структуры luam_Metrics
тип данных — size_t
. Функция luaM_metrics()
заполняет структуру *metrics
метриками, относящимися к Lua-состоянию, которое связано с корутиной L
.
Пример с программой на языке C
В руководстве Tarantool по хранимым процедурам на языке C перейдите к примеру с файлом easy.c
. Удалите содержимое файла и вставьте следующий код:
#include "module.h"
#include <lmisclib.h>
int easy(box_function_ctx_t *ctx, const char *args, const char *args_end)
{
lua_State *ls = luaT_state();
struct luam_Metrics m;
luaM_metrics(ls, &m);
printf("allocated memory = %lu\n", m.gc_allocated);
return 0;
}
Теперь, как в исходном примере, выполните через клиент запросы до capi_connection:call('easy')
включительно. На экране появится следующее: allocated memory = 4431950
(число приведено для примера).
Пример с gc_strnum, strhash_miss и strhash_hit
Отслеживать размещение новых строковых объектов можно так:
function f()
collectgarbage("collect")
local oldm = misc.getmetrics()
local table_of_strings = {}
for i = 3000, 4000 do table.insert(table_of_strings, tostring(i)) end
for i = 3900, 4100 do table.insert(table_of_strings, tostring(i)) end
local newm = misc.getmetrics()
print("gc_strnum diff = " .. newm.gc_strnum - oldm.gc_strnum)
print("strhash_miss diff = " .. newm.strhash_miss - oldm.strhash_miss)
print("strhash_hit diff = " .. newm.strhash_hit - oldm.strhash_hit)
end
f()
Вероятный результат — gc_strnum diff = 1101
, так как мы добавили 1202 строки, 101 из которых были дубликатами. По той же причине strhash_miss = 1101
, а strhash_hit = 101
плюс некоторые издержки. (strhash_hit
всегда предполагает небольшие издержки, которые можно игнорировать.)
Результат лишь вероятный, поскольку память для строк могла быть выделена ранее. Хорошо, если кривая наклона strhash_miss
менее крутая, чем у strhash_hit
.
Доступ к остальным значениям gc_*num
— gc_cdatanum
, gc_tabnum
и gc_udatanum
можно получить аналогичным образом. Любое значение gc_*num
поможет при поиске утечки памяти: общее количество этих объектов не должно постоянно расти. Более общий способ искать утечки памяти — наблюдать за переменной gc_total
. Также можно отслеживать значение jit_mcode_size
, отражающее объем памяти, выделенной для трасс машинного кода.
Пример с gc_allocated и gc_freed
Чем меньше работает сборщик мусора, тем лучше. Отслеживать, насколько его нагружает приложение, можно так:
function f()
for i = 1, 10 do collectgarbage("collect") end
local oldm = misc.getmetrics()
local newm = misc.getmetrics()
oldm = misc.getmetrics()
collectgarbage("collect")
newm = misc.getmetrics()
print("gc_allocated diff = " .. newm.gc_allocated - oldm.gc_allocated)
print("gc_freed diff = " .. newm.gc_freed - oldm.gc_freed)
end
f()
Результат: gc_allocated diff = 800
, gc_freed diff = 800
. Отсюда видно, что строка local ... = getmetrics()
вызывает аллокацию памяти, поскольку создает таблицу и наполняет ее значениями. Когда имя переменной (в данном случае oldm
) используется повторно, память освобождается. Обычно это происходит не сразу, однако collectgarbage("collect")
принудительно запускает сборку мусора, благодаря чему можно немедленно увидеть результат.
Пример с gc_allocated и оптимизацией по памяти
Этот пример показывает, что можно оптимизировать расход памяти для таблиц.
function f()
collectgarbage("collect")
local oldm = misc.getmetrics()
local t = {}
for i = 1, 513 do
t[i] = i
end
local newm = misc.getmetrics()
local diff = newm.gc_allocated - oldm.gc_allocated
print("diff = " .. diff)
end
f()
Результат покажет, что значение diff примерно равно 18000.
Если инициализировать таблицу по-другому, получится вот что:
function f()
local table_new = require "table.new"
local oldm = misc.getmetrics()
local t = table_new(513, 0)
for i = 1, 513 do
t[i] = i
end
local newm = misc.getmetrics()
local diff = newm.gc_allocated - oldm.gc_allocated
print("diff = " .. diff)
end
f()
Результат покажет, что значение diff примерно равно 6000.
gc_steps_atomic и gc_steps_propagate
Нагрузку на сборщик мусора помогают отслеживать кривые наклона метрик gc_steps_*
. Во время долго выполняющихся процедур значения gc_steps_*
увеличиваются. При этом большие промежутки времени между увеличениями gc_steps_atomic
— хороший признак. Поскольку gc_steps_atomic
увеличивается только раз в цикл сбора мусора, можно увидеть, сколько циклов прошло.
Кроме того, по тому, насколько выросло значение gc_steps_propagate
, можно косвенно оценить количество объектов. Это значение также коррелирует с множителем шага сборщика мусора. Например, количество инкрементальных шагов может увеличиваться, однако множитель шага настроен так, что за каждый шаг можно обработать лишь небольшое количество объектов. Поэтому при настройке сборщика мусора следует учитывать эти показатели.
Следующая функция оценивает, вызывает ли оператор SQL большую нагрузку:
function f()
collectgarbage("collect")
local oldm = misc.getmetrics()
collectgarbage("collect")
box.execute([[DROP TABLE _vindex;]])
local newm = misc.getmetrics()
print("gc_steps_atomic = " .. newm.gc_steps_atomic - oldm.gc_steps_atomic)
print("gc_steps_finalize = " .. newm.gc_steps_finalize - oldm.gc_steps_finalize)
print("gc_steps_pause = " .. newm.gc_steps_pause - oldm.gc_steps_pause)
print("gc_steps_propagate = " .. newm.gc_steps_propagate - oldm.gc_steps_propagate)
print("gc_steps_sweep = " .. newm.gc_steps_sweep - oldm.gc_steps_sweep)
end
f()
Очевидно, значения метрик gc_steps_ *
, полученные до вызова box.execute()
, существенно не отличаются от значений, полученных после этого вызова.
Пример с jit_trace_num и jit_trace_abort
JIT-компиляторы трассируют код, пытаясь найти возможность для компиляции, чтобы повысить производительность. Значение jit_trace_abort
показывает, как часто эти попытки оканчиваются неудачей (чем меньше это значение, тем лучше), а jit_trace_num
— как много трассировок сгенерировано с момента последнего выполнения операции flush (обычно чем больше значение, тем лучше).
Код следующей функции будет успешно трассирован:
function f()
jit.flush()
for i = 1, 10 do collectgarbage("collect") end
local oldm = misc.getmetrics()
collectgarbage("collect")
local sum = 0
for i = 1, 57 do
sum = sum + 57
end
for i = 1, 10 do collectgarbage("collect") end
local newm = misc.getmetrics()
print("trace_num = " .. newm.jit_trace_num - oldm.jit_trace_num)
print("trace_abort = " .. newm.jit_trace_abort - oldm.jit_trace_abort)
end
f()
Результат: trace_num = 1
, trace_abort = 0
. Отлично.
А в следующей функции, похоже, есть код, способный вызвать у LuaJIT проблемы:
jit.opt.start(0, "hotloop=2", "hotexit=2", "minstitch=15")
_G.globalthing = 5
function f()
jit.flush()
collectgarbage("collect")
local oldm = misc.getmetrics()
collectgarbage("collect")
local sum = 0
for i = 1, box.space._vindex:count()+ _G.globalthing do
box.execute([[SELECT RANDOMBLOB(0);]])
require('buffer').ibuf()
_G.globalthing = _G.globalthing - 1
end
local newm = misc.getmetrics()
print("trace_num = " .. newm.jit_trace_num - oldm.jit_trace_num)
print("trace_abort = " .. newm.jit_trace_abort - oldm.jit_trace_abort)
end
f()
Результат: trace_num
— от 2
до 4
, trace_abort = 1
. Это означает, что нужно было сгенерировать до четырех трассировок вместо одной, причем что-то заставило LuaJIT прекратить попытки. Дальнейшая трассировка показывает, что проблема не в подозрительно выглядящих операторах внутри функции, а в вызове jit.opt.start
. (Содержимое файла jit.dump
может помочь разобраться, как протекала компиляция трасс.)
Пример с jit_snap_restore и деоптимизацией производительности
Если кривая наклона метрики jit_snap_restore
после изменений в старом коде растет, это может означать, что LuaJIT чаще останавливает выполнение кода на трассах. А это, в свою очередь, может указывать на снижение производительности.
Рассмотрим следующий код:
function f()
local function foo(i)
return i <= 5 and i or tostring(i)
end
-- параметр minstitch нужен, чтобы эмулировать поведение non-stitching
jit.opt.start(0, "hotloop=2", "hotexit=2", "minstitch=15")
local sum = 0
local oldm = misc.getmetrics()
for i = 1, 10 do
sum = sum + foo(i)
end
local newm = misc.getmetrics()
local diff = newm.jit_snap_restore - oldm.jit_snap_restore
print("diff = " .. diff)
end
f()
Результат: diff = 3
, поскольку при выполнении трассы возможны три сторонних выхода. Один находится в конце цикла. Другие два переключают выполнение на интерпретатор, прежде чем LuaJIT решит, что фрагмент кода «горячий» (значение параметра hotloop
по умолчанию равно 56
согласно документации LuaJIT).
А теперь изменим единственную строку в функции local foo
, чтобы получить следующий код:
function f()
local function foo(i)
-- функция math.fmod еще не скомпилирована
return i <= 5 and i or math.fmod(i, 11)
end
-- параметр minstitch нужен, чтобы эмулировать поведение non-stitching
jit.opt.start(0, "hotloop=2", "hotexit=2", "minstitch=15")
local sum = 0
local oldm = misc.getmetrics()
for i = 1, 10 do
sum = sum + foo(i)
end
local newm = misc.getmetrics()
local diff = newm.jit_snap_restore - oldm.jit_snap_restore
print("diff = " .. diff)
end
f()
Результат: значение diff
увеличилось, так как сторонних выходов стало больше. Этот тест показывает, что измение кода влияет на производительность.
Отслеживать размещение новых строковых объектов можно так:
function f()
collectgarbage("collect")
local oldm = misc.getmetrics()
local table_of_strings = {}
for i = 3000, 4000 do table.insert(table_of_strings, tostring(i)) end
for i = 3900, 4100 do table.insert(table_of_strings, tostring(i)) end
local newm = misc.getmetrics()
print("gc_strnum diff = " .. newm.gc_strnum - oldm.gc_strnum)
print("strhash_miss diff = " .. newm.strhash_miss - oldm.strhash_miss)
print("strhash_hit diff = " .. newm.strhash_hit - oldm.strhash_hit)
end
f()
Вероятный результат — gc_strnum diff = 1101
, так как мы добавили 1202 строки, 101 из которых были дубликатами. По той же причине strhash_miss = 1101
, а strhash_hit = 101
плюс некоторые издержки. (strhash_hit
всегда предполагает небольшие издержки, которые можно игнорировать.)
Результат лишь вероятный, поскольку память для строк могла быть выделена ранее. Хорошо, если кривая наклона strhash_miss
менее крутая, чем у strhash_hit
.
Доступ к остальным значениям gc_*num
— gc_cdatanum
, gc_tabnum
и gc_udatanum
можно получить аналогичным образом. Любое значение gc_*num
поможет при поиске утечки памяти: общее количество этих объектов не должно постоянно расти. Более общий способ искать утечки памяти — наблюдать за переменной gc_total
. Также можно отслеживать значение jit_mcode_size
, отражающее объем памяти, выделенной для трасс машинного кода.
Пример с gc_allocated и gc_freed
Чем меньше работает сборщик мусора, тем лучше. Отслеживать, насколько его нагружает приложение, можно так:
function f()
for i = 1, 10 do collectgarbage("collect") end
local oldm = misc.getmetrics()
local newm = misc.getmetrics()
oldm = misc.getmetrics()
collectgarbage("collect")
newm = misc.getmetrics()
print("gc_allocated diff = " .. newm.gc_allocated - oldm.gc_allocated)
print("gc_freed diff = " .. newm.gc_freed - oldm.gc_freed)
end
f()
Результат: gc_allocated diff = 800
, gc_freed diff = 800
. Отсюда видно, что строка local ... = getmetrics()
вызывает аллокацию памяти, поскольку создает таблицу и наполняет ее значениями. Когда имя переменной (в данном случае oldm
) используется повторно, память освобождается. Обычно это происходит не сразу, однако collectgarbage("collect")
принудительно запускает сборку мусора, благодаря чему можно немедленно увидеть результат.
Пример с gc_allocated и оптимизацией по памяти
Этот пример показывает, что можно оптимизировать расход памяти для таблиц.
function f()
collectgarbage("collect")
local oldm = misc.getmetrics()
local t = {}
for i = 1, 513 do
t[i] = i
end
local newm = misc.getmetrics()
local diff = newm.gc_allocated - oldm.gc_allocated
print("diff = " .. diff)
end
f()
Результат покажет, что значение diff примерно равно 18000.
Если инициализировать таблицу по-другому, получится вот что:
function f()
local table_new = require "table.new"
local oldm = misc.getmetrics()
local t = table_new(513, 0)
for i = 1, 513 do
t[i] = i
end
local newm = misc.getmetrics()
local diff = newm.gc_allocated - oldm.gc_allocated
print("diff = " .. diff)
end
f()
Результат покажет, что значение diff примерно равно 6000.
gc_steps_atomic и gc_steps_propagate
Нагрузку на сборщик мусора помогают отслеживать кривые наклона метрик gc_steps_*
. Во время долго выполняющихся процедур значения gc_steps_*
увеличиваются. При этом большие промежутки времени между увеличениями gc_steps_atomic
— хороший признак. Поскольку gc_steps_atomic
увеличивается только раз в цикл сбора мусора, можно увидеть, сколько циклов прошло.
Кроме того, по тому, насколько выросло значение gc_steps_propagate
, можно косвенно оценить количество объектов. Это значение также коррелирует с множителем шага сборщика мусора. Например, количество инкрементальных шагов может увеличиваться, однако множитель шага настроен так, что за каждый шаг можно обработать лишь небольшое количество объектов. Поэтому при настройке сборщика мусора следует учитывать эти показатели.
Следующая функция оценивает, вызывает ли оператор SQL большую нагрузку:
function f()
collectgarbage("collect")
local oldm = misc.getmetrics()
collectgarbage("collect")
box.execute([[DROP TABLE _vindex;]])
local newm = misc.getmetrics()
print("gc_steps_atomic = " .. newm.gc_steps_atomic - oldm.gc_steps_atomic)
print("gc_steps_finalize = " .. newm.gc_steps_finalize - oldm.gc_steps_finalize)
print("gc_steps_pause = " .. newm.gc_steps_pause - oldm.gc_steps_pause)
print("gc_steps_propagate = " .. newm.gc_steps_propagate - oldm.gc_steps_propagate)
print("gc_steps_sweep = " .. newm.gc_steps_sweep - oldm.gc_steps_sweep)
end
f()
Очевидно, значения метрик gc_steps_ *
, полученные до вызова box.execute()
, существенно не отличаются от значений, полученных после этого вызова.
Пример с jit_trace_num и jit_trace_abort
JIT-компиляторы трассируют код, пытаясь найти возможность для компиляции, чтобы повысить производительность. Значение jit_trace_abort
показывает, как часто эти попытки оканчиваются неудачей (чем меньше это значение, тем лучше), а jit_trace_num
— как много трассировок сгенерировано с момента последнего выполнения операции flush (обычно чем больше значение, тем лучше).
Код следующей функции будет успешно трассирован:
function f()
jit.flush()
for i = 1, 10 do collectgarbage("collect") end
local oldm = misc.getmetrics()
collectgarbage("collect")
local sum = 0
for i = 1, 57 do
sum = sum + 57
end
for i = 1, 10 do collectgarbage("collect") end
local newm = misc.getmetrics()
print("trace_num = " .. newm.jit_trace_num - oldm.jit_trace_num)
print("trace_abort = " .. newm.jit_trace_abort - oldm.jit_trace_abort)
end
f()
Результат: trace_num = 1
, trace_abort = 0
. Отлично.
А в следующей функции, похоже, есть код, способный вызвать у LuaJIT проблемы:
jit.opt.start(0, "hotloop=2", "hotexit=2", "minstitch=15")
_G.globalthing = 5
function f()
jit.flush()
collectgarbage("collect")
local oldm = misc.getmetrics()
collectgarbage("collect")
local sum = 0
for i = 1, box.space._vindex:count()+ _G.globalthing do
box.execute([[SELECT RANDOMBLOB(0);]])
require('buffer').ibuf()
_G.globalthing = _G.globalthing - 1
end
local newm = misc.getmetrics()
print("trace_num = " .. newm.jit_trace_num - oldm.jit_trace_num)
print("trace_abort = " .. newm.jit_trace_abort - oldm.jit_trace_abort)
end
f()
Результат: trace_num
— от 2
до 4
, trace_abort = 1
. Это означает, что нужно было сгенерировать до четырех трассировок вместо одной, причем что-то заставило LuaJIT прекратить попытки. Дальнейшая трассировка показывает, что проблема не в подозрительно выглядящих операторах внутри функции, а в вызове jit.opt.start
. (Содержимое файла jit.dump
может помочь разобраться, как протекала компиляция трасс.)
Пример с jit_snap_restore и деоптимизацией производительности
Если кривая наклона метрики jit_snap_restore
после изменений в старом коде растет, это может означать, что LuaJIT чаще останавливает выполнение кода на трассах. А это, в свою очередь, может указывать на снижение производительности.
Рассмотрим следующий код:
function f()
local function foo(i)
return i <= 5 and i or tostring(i)
end
-- параметр minstitch нужен, чтобы эмулировать поведение non-stitching
jit.opt.start(0, "hotloop=2", "hotexit=2", "minstitch=15")
local sum = 0
local oldm = misc.getmetrics()
for i = 1, 10 do
sum = sum + foo(i)
end
local newm = misc.getmetrics()
local diff = newm.jit_snap_restore - oldm.jit_snap_restore
print("diff = " .. diff)
end
f()
Результат: diff = 3
, поскольку при выполнении трассы возможны три сторонних выхода. Один находится в конце цикла. Другие два переключают выполнение на интерпретатор, прежде чем LuaJIT решит, что фрагмент кода «горячий» (значение параметра hotloop
по умолчанию равно 56
согласно документации LuaJIT).
А теперь изменим единственную строку в функции local foo
, чтобы получить следующий код:
function f()
local function foo(i)
-- функция math.fmod еще не скомпилирована
return i <= 5 and i or math.fmod(i, 11)
end
-- параметр minstitch нужен, чтобы эмулировать поведение non-stitching
jit.opt.start(0, "hotloop=2", "hotexit=2", "minstitch=15")
local sum = 0
local oldm = misc.getmetrics()
for i = 1, 10 do
sum = sum + foo(i)
end
local newm = misc.getmetrics()
local diff = newm.jit_snap_restore - oldm.jit_snap_restore
print("diff = " .. diff)
end
f()
Результат: значение diff
увеличилось, так как сторонних выходов стало больше. Этот тест показывает, что измение кода влияет на производительность.
Чем меньше работает сборщик мусора, тем лучше. Отслеживать, насколько его нагружает приложение, можно так:
function f()
for i = 1, 10 do collectgarbage("collect") end
local oldm = misc.getmetrics()
local newm = misc.getmetrics()
oldm = misc.getmetrics()
collectgarbage("collect")
newm = misc.getmetrics()
print("gc_allocated diff = " .. newm.gc_allocated - oldm.gc_allocated)
print("gc_freed diff = " .. newm.gc_freed - oldm.gc_freed)
end
f()
Результат: gc_allocated diff = 800
, gc_freed diff = 800
. Отсюда видно, что строка local ... = getmetrics()
вызывает аллокацию памяти, поскольку создает таблицу и наполняет ее значениями. Когда имя переменной (в данном случае oldm
) используется повторно, память освобождается. Обычно это происходит не сразу, однако collectgarbage("collect")
принудительно запускает сборку мусора, благодаря чему можно немедленно увидеть результат.
Пример с gc_allocated и оптимизацией по памяти
Этот пример показывает, что можно оптимизировать расход памяти для таблиц.
function f()
collectgarbage("collect")
local oldm = misc.getmetrics()
local t = {}
for i = 1, 513 do
t[i] = i
end
local newm = misc.getmetrics()
local diff = newm.gc_allocated - oldm.gc_allocated
print("diff = " .. diff)
end
f()
Результат покажет, что значение diff примерно равно 18000.
Если инициализировать таблицу по-другому, получится вот что:
function f()
local table_new = require "table.new"
local oldm = misc.getmetrics()
local t = table_new(513, 0)
for i = 1, 513 do
t[i] = i
end
local newm = misc.getmetrics()
local diff = newm.gc_allocated - oldm.gc_allocated
print("diff = " .. diff)
end
f()
Результат покажет, что значение diff примерно равно 6000.
gc_steps_atomic и gc_steps_propagate
Нагрузку на сборщик мусора помогают отслеживать кривые наклона метрик gc_steps_*
. Во время долго выполняющихся процедур значения gc_steps_*
увеличиваются. При этом большие промежутки времени между увеличениями gc_steps_atomic
— хороший признак. Поскольку gc_steps_atomic
увеличивается только раз в цикл сбора мусора, можно увидеть, сколько циклов прошло.
Кроме того, по тому, насколько выросло значение gc_steps_propagate
, можно косвенно оценить количество объектов. Это значение также коррелирует с множителем шага сборщика мусора. Например, количество инкрементальных шагов может увеличиваться, однако множитель шага настроен так, что за каждый шаг можно обработать лишь небольшое количество объектов. Поэтому при настройке сборщика мусора следует учитывать эти показатели.
Следующая функция оценивает, вызывает ли оператор SQL большую нагрузку:
function f()
collectgarbage("collect")
local oldm = misc.getmetrics()
collectgarbage("collect")
box.execute([[DROP TABLE _vindex;]])
local newm = misc.getmetrics()
print("gc_steps_atomic = " .. newm.gc_steps_atomic - oldm.gc_steps_atomic)
print("gc_steps_finalize = " .. newm.gc_steps_finalize - oldm.gc_steps_finalize)
print("gc_steps_pause = " .. newm.gc_steps_pause - oldm.gc_steps_pause)
print("gc_steps_propagate = " .. newm.gc_steps_propagate - oldm.gc_steps_propagate)
print("gc_steps_sweep = " .. newm.gc_steps_sweep - oldm.gc_steps_sweep)
end
f()
Очевидно, значения метрик gc_steps_ *
, полученные до вызова box.execute()
, существенно не отличаются от значений, полученных после этого вызова.
Пример с jit_trace_num и jit_trace_abort
JIT-компиляторы трассируют код, пытаясь найти возможность для компиляции, чтобы повысить производительность. Значение jit_trace_abort
показывает, как часто эти попытки оканчиваются неудачей (чем меньше это значение, тем лучше), а jit_trace_num
— как много трассировок сгенерировано с момента последнего выполнения операции flush (обычно чем больше значение, тем лучше).
Код следующей функции будет успешно трассирован:
function f()
jit.flush()
for i = 1, 10 do collectgarbage("collect") end
local oldm = misc.getmetrics()
collectgarbage("collect")
local sum = 0
for i = 1, 57 do
sum = sum + 57
end
for i = 1, 10 do collectgarbage("collect") end
local newm = misc.getmetrics()
print("trace_num = " .. newm.jit_trace_num - oldm.jit_trace_num)
print("trace_abort = " .. newm.jit_trace_abort - oldm.jit_trace_abort)
end
f()
Результат: trace_num = 1
, trace_abort = 0
. Отлично.
А в следующей функции, похоже, есть код, способный вызвать у LuaJIT проблемы:
jit.opt.start(0, "hotloop=2", "hotexit=2", "minstitch=15")
_G.globalthing = 5
function f()
jit.flush()
collectgarbage("collect")
local oldm = misc.getmetrics()
collectgarbage("collect")
local sum = 0
for i = 1, box.space._vindex:count()+ _G.globalthing do
box.execute([[SELECT RANDOMBLOB(0);]])
require('buffer').ibuf()
_G.globalthing = _G.globalthing - 1
end
local newm = misc.getmetrics()
print("trace_num = " .. newm.jit_trace_num - oldm.jit_trace_num)
print("trace_abort = " .. newm.jit_trace_abort - oldm.jit_trace_abort)
end
f()
Результат: trace_num
— от 2
до 4
, trace_abort = 1
. Это означает, что нужно было сгенерировать до четырех трассировок вместо одной, причем что-то заставило LuaJIT прекратить попытки. Дальнейшая трассировка показывает, что проблема не в подозрительно выглядящих операторах внутри функции, а в вызове jit.opt.start
. (Содержимое файла jit.dump
может помочь разобраться, как протекала компиляция трасс.)
Пример с jit_snap_restore и деоптимизацией производительности
Если кривая наклона метрики jit_snap_restore
после изменений в старом коде растет, это может означать, что LuaJIT чаще останавливает выполнение кода на трассах. А это, в свою очередь, может указывать на снижение производительности.
Рассмотрим следующий код:
function f()
local function foo(i)
return i <= 5 and i or tostring(i)
end
-- параметр minstitch нужен, чтобы эмулировать поведение non-stitching
jit.opt.start(0, "hotloop=2", "hotexit=2", "minstitch=15")
local sum = 0
local oldm = misc.getmetrics()
for i = 1, 10 do
sum = sum + foo(i)
end
local newm = misc.getmetrics()
local diff = newm.jit_snap_restore - oldm.jit_snap_restore
print("diff = " .. diff)
end
f()
Результат: diff = 3
, поскольку при выполнении трассы возможны три сторонних выхода. Один находится в конце цикла. Другие два переключают выполнение на интерпретатор, прежде чем LuaJIT решит, что фрагмент кода «горячий» (значение параметра hotloop
по умолчанию равно 56
согласно документации LuaJIT).
А теперь изменим единственную строку в функции local foo
, чтобы получить следующий код:
function f()
local function foo(i)
-- функция math.fmod еще не скомпилирована
return i <= 5 and i or math.fmod(i, 11)
end
-- параметр minstitch нужен, чтобы эмулировать поведение non-stitching
jit.opt.start(0, "hotloop=2", "hotexit=2", "minstitch=15")
local sum = 0
local oldm = misc.getmetrics()
for i = 1, 10 do
sum = sum + foo(i)
end
local newm = misc.getmetrics()
local diff = newm.jit_snap_restore - oldm.jit_snap_restore
print("diff = " .. diff)
end
f()
Результат: значение diff
увеличилось, так как сторонних выходов стало больше. Этот тест показывает, что измение кода влияет на производительность.
Этот пример показывает, что можно оптимизировать расход памяти для таблиц.
function f()
collectgarbage("collect")
local oldm = misc.getmetrics()
local t = {}
for i = 1, 513 do
t[i] = i
end
local newm = misc.getmetrics()
local diff = newm.gc_allocated - oldm.gc_allocated
print("diff = " .. diff)
end
f()
Результат покажет, что значение diff примерно равно 18000.
Если инициализировать таблицу по-другому, получится вот что:
function f()
local table_new = require "table.new"
local oldm = misc.getmetrics()
local t = table_new(513, 0)
for i = 1, 513 do
t[i] = i
end
local newm = misc.getmetrics()
local diff = newm.gc_allocated - oldm.gc_allocated
print("diff = " .. diff)
end
f()
Результат покажет, что значение diff примерно равно 6000.
gc_steps_atomic и gc_steps_propagate
Нагрузку на сборщик мусора помогают отслеживать кривые наклона метрик gc_steps_*
. Во время долго выполняющихся процедур значения gc_steps_*
увеличиваются. При этом большие промежутки времени между увеличениями gc_steps_atomic
— хороший признак. Поскольку gc_steps_atomic
увеличивается только раз в цикл сбора мусора, можно увидеть, сколько циклов прошло.
Кроме того, по тому, насколько выросло значение gc_steps_propagate
, можно косвенно оценить количество объектов. Это значение также коррелирует с множителем шага сборщика мусора. Например, количество инкрементальных шагов может увеличиваться, однако множитель шага настроен так, что за каждый шаг можно обработать лишь небольшое количество объектов. Поэтому при настройке сборщика мусора следует учитывать эти показатели.
Следующая функция оценивает, вызывает ли оператор SQL большую нагрузку:
function f()
collectgarbage("collect")
local oldm = misc.getmetrics()
collectgarbage("collect")
box.execute([[DROP TABLE _vindex;]])
local newm = misc.getmetrics()
print("gc_steps_atomic = " .. newm.gc_steps_atomic - oldm.gc_steps_atomic)
print("gc_steps_finalize = " .. newm.gc_steps_finalize - oldm.gc_steps_finalize)
print("gc_steps_pause = " .. newm.gc_steps_pause - oldm.gc_steps_pause)
print("gc_steps_propagate = " .. newm.gc_steps_propagate - oldm.gc_steps_propagate)
print("gc_steps_sweep = " .. newm.gc_steps_sweep - oldm.gc_steps_sweep)
end
f()
Очевидно, значения метрик gc_steps_ *
, полученные до вызова box.execute()
, существенно не отличаются от значений, полученных после этого вызова.
Пример с jit_trace_num и jit_trace_abort
JIT-компиляторы трассируют код, пытаясь найти возможность для компиляции, чтобы повысить производительность. Значение jit_trace_abort
показывает, как часто эти попытки оканчиваются неудачей (чем меньше это значение, тем лучше), а jit_trace_num
— как много трассировок сгенерировано с момента последнего выполнения операции flush (обычно чем больше значение, тем лучше).
Код следующей функции будет успешно трассирован:
function f()
jit.flush()
for i = 1, 10 do collectgarbage("collect") end
local oldm = misc.getmetrics()
collectgarbage("collect")
local sum = 0
for i = 1, 57 do
sum = sum + 57
end
for i = 1, 10 do collectgarbage("collect") end
local newm = misc.getmetrics()
print("trace_num = " .. newm.jit_trace_num - oldm.jit_trace_num)
print("trace_abort = " .. newm.jit_trace_abort - oldm.jit_trace_abort)
end
f()
Результат: trace_num = 1
, trace_abort = 0
. Отлично.
А в следующей функции, похоже, есть код, способный вызвать у LuaJIT проблемы:
jit.opt.start(0, "hotloop=2", "hotexit=2", "minstitch=15")
_G.globalthing = 5
function f()
jit.flush()
collectgarbage("collect")
local oldm = misc.getmetrics()
collectgarbage("collect")
local sum = 0
for i = 1, box.space._vindex:count()+ _G.globalthing do
box.execute([[SELECT RANDOMBLOB(0);]])
require('buffer').ibuf()
_G.globalthing = _G.globalthing - 1
end
local newm = misc.getmetrics()
print("trace_num = " .. newm.jit_trace_num - oldm.jit_trace_num)
print("trace_abort = " .. newm.jit_trace_abort - oldm.jit_trace_abort)
end
f()
Результат: trace_num
— от 2
до 4
, trace_abort = 1
. Это означает, что нужно было сгенерировать до четырех трассировок вместо одной, причем что-то заставило LuaJIT прекратить попытки. Дальнейшая трассировка показывает, что проблема не в подозрительно выглядящих операторах внутри функции, а в вызове jit.opt.start
. (Содержимое файла jit.dump
может помочь разобраться, как протекала компиляция трасс.)
Пример с jit_snap_restore и деоптимизацией производительности
Если кривая наклона метрики jit_snap_restore
после изменений в старом коде растет, это может означать, что LuaJIT чаще останавливает выполнение кода на трассах. А это, в свою очередь, может указывать на снижение производительности.
Рассмотрим следующий код:
function f()
local function foo(i)
return i <= 5 and i or tostring(i)
end
-- параметр minstitch нужен, чтобы эмулировать поведение non-stitching
jit.opt.start(0, "hotloop=2", "hotexit=2", "minstitch=15")
local sum = 0
local oldm = misc.getmetrics()
for i = 1, 10 do
sum = sum + foo(i)
end
local newm = misc.getmetrics()
local diff = newm.jit_snap_restore - oldm.jit_snap_restore
print("diff = " .. diff)
end
f()
Результат: diff = 3
, поскольку при выполнении трассы возможны три сторонних выхода. Один находится в конце цикла. Другие два переключают выполнение на интерпретатор, прежде чем LuaJIT решит, что фрагмент кода «горячий» (значение параметра hotloop
по умолчанию равно 56
согласно документации LuaJIT).
А теперь изменим единственную строку в функции local foo
, чтобы получить следующий код:
function f()
local function foo(i)
-- функция math.fmod еще не скомпилирована
return i <= 5 and i or math.fmod(i, 11)
end
-- параметр minstitch нужен, чтобы эмулировать поведение non-stitching
jit.opt.start(0, "hotloop=2", "hotexit=2", "minstitch=15")
local sum = 0
local oldm = misc.getmetrics()
for i = 1, 10 do
sum = sum + foo(i)
end
local newm = misc.getmetrics()
local diff = newm.jit_snap_restore - oldm.jit_snap_restore
print("diff = " .. diff)
end
f()
Результат: значение diff
увеличилось, так как сторонних выходов стало больше. Этот тест показывает, что измение кода влияет на производительность.
Нагрузку на сборщик мусора помогают отслеживать кривые наклона метрик gc_steps_*
. Во время долго выполняющихся процедур значения gc_steps_*
увеличиваются. При этом большие промежутки времени между увеличениями gc_steps_atomic
— хороший признак. Поскольку gc_steps_atomic
увеличивается только раз в цикл сбора мусора, можно увидеть, сколько циклов прошло.
Кроме того, по тому, насколько выросло значение gc_steps_propagate
, можно косвенно оценить количество объектов. Это значение также коррелирует с множителем шага сборщика мусора. Например, количество инкрементальных шагов может увеличиваться, однако множитель шага настроен так, что за каждый шаг можно обработать лишь небольшое количество объектов. Поэтому при настройке сборщика мусора следует учитывать эти показатели.
Следующая функция оценивает, вызывает ли оператор SQL большую нагрузку:
function f()
collectgarbage("collect")
local oldm = misc.getmetrics()
collectgarbage("collect")
box.execute([[DROP TABLE _vindex;]])
local newm = misc.getmetrics()
print("gc_steps_atomic = " .. newm.gc_steps_atomic - oldm.gc_steps_atomic)
print("gc_steps_finalize = " .. newm.gc_steps_finalize - oldm.gc_steps_finalize)
print("gc_steps_pause = " .. newm.gc_steps_pause - oldm.gc_steps_pause)
print("gc_steps_propagate = " .. newm.gc_steps_propagate - oldm.gc_steps_propagate)
print("gc_steps_sweep = " .. newm.gc_steps_sweep - oldm.gc_steps_sweep)
end
f()
Очевидно, значения метрик gc_steps_ *
, полученные до вызова box.execute()
, существенно не отличаются от значений, полученных после этого вызова.
Пример с jit_trace_num и jit_trace_abort
JIT-компиляторы трассируют код, пытаясь найти возможность для компиляции, чтобы повысить производительность. Значение jit_trace_abort
показывает, как часто эти попытки оканчиваются неудачей (чем меньше это значение, тем лучше), а jit_trace_num
— как много трассировок сгенерировано с момента последнего выполнения операции flush (обычно чем больше значение, тем лучше).
Код следующей функции будет успешно трассирован:
function f()
jit.flush()
for i = 1, 10 do collectgarbage("collect") end
local oldm = misc.getmetrics()
collectgarbage("collect")
local sum = 0
for i = 1, 57 do
sum = sum + 57
end
for i = 1, 10 do collectgarbage("collect") end
local newm = misc.getmetrics()
print("trace_num = " .. newm.jit_trace_num - oldm.jit_trace_num)
print("trace_abort = " .. newm.jit_trace_abort - oldm.jit_trace_abort)
end
f()
Результат: trace_num = 1
, trace_abort = 0
. Отлично.
А в следующей функции, похоже, есть код, способный вызвать у LuaJIT проблемы:
jit.opt.start(0, "hotloop=2", "hotexit=2", "minstitch=15")
_G.globalthing = 5
function f()
jit.flush()
collectgarbage("collect")
local oldm = misc.getmetrics()
collectgarbage("collect")
local sum = 0
for i = 1, box.space._vindex:count()+ _G.globalthing do
box.execute([[SELECT RANDOMBLOB(0);]])
require('buffer').ibuf()
_G.globalthing = _G.globalthing - 1
end
local newm = misc.getmetrics()
print("trace_num = " .. newm.jit_trace_num - oldm.jit_trace_num)
print("trace_abort = " .. newm.jit_trace_abort - oldm.jit_trace_abort)
end
f()
Результат: trace_num
— от 2
до 4
, trace_abort = 1
. Это означает, что нужно было сгенерировать до четырех трассировок вместо одной, причем что-то заставило LuaJIT прекратить попытки. Дальнейшая трассировка показывает, что проблема не в подозрительно выглядящих операторах внутри функции, а в вызове jit.opt.start
. (Содержимое файла jit.dump
может помочь разобраться, как протекала компиляция трасс.)
Пример с jit_snap_restore и деоптимизацией производительности
Если кривая наклона метрики jit_snap_restore
после изменений в старом коде растет, это может означать, что LuaJIT чаще останавливает выполнение кода на трассах. А это, в свою очередь, может указывать на снижение производительности.
Рассмотрим следующий код:
function f()
local function foo(i)
return i <= 5 and i or tostring(i)
end
-- параметр minstitch нужен, чтобы эмулировать поведение non-stitching
jit.opt.start(0, "hotloop=2", "hotexit=2", "minstitch=15")
local sum = 0
local oldm = misc.getmetrics()
for i = 1, 10 do
sum = sum + foo(i)
end
local newm = misc.getmetrics()
local diff = newm.jit_snap_restore - oldm.jit_snap_restore
print("diff = " .. diff)
end
f()
Результат: diff = 3
, поскольку при выполнении трассы возможны три сторонних выхода. Один находится в конце цикла. Другие два переключают выполнение на интерпретатор, прежде чем LuaJIT решит, что фрагмент кода «горячий» (значение параметра hotloop
по умолчанию равно 56
согласно документации LuaJIT).
А теперь изменим единственную строку в функции local foo
, чтобы получить следующий код:
function f()
local function foo(i)
-- функция math.fmod еще не скомпилирована
return i <= 5 and i or math.fmod(i, 11)
end
-- параметр minstitch нужен, чтобы эмулировать поведение non-stitching
jit.opt.start(0, "hotloop=2", "hotexit=2", "minstitch=15")
local sum = 0
local oldm = misc.getmetrics()
for i = 1, 10 do
sum = sum + foo(i)
end
local newm = misc.getmetrics()
local diff = newm.jit_snap_restore - oldm.jit_snap_restore
print("diff = " .. diff)
end
f()
Результат: значение diff
увеличилось, так как сторонних выходов стало больше. Этот тест показывает, что измение кода влияет на производительность.
JIT-компиляторы трассируют код, пытаясь найти возможность для компиляции, чтобы повысить производительность. Значение jit_trace_abort
показывает, как часто эти попытки оканчиваются неудачей (чем меньше это значение, тем лучше), а jit_trace_num
— как много трассировок сгенерировано с момента последнего выполнения операции flush (обычно чем больше значение, тем лучше).
Код следующей функции будет успешно трассирован:
function f()
jit.flush()
for i = 1, 10 do collectgarbage("collect") end
local oldm = misc.getmetrics()
collectgarbage("collect")
local sum = 0
for i = 1, 57 do
sum = sum + 57
end
for i = 1, 10 do collectgarbage("collect") end
local newm = misc.getmetrics()
print("trace_num = " .. newm.jit_trace_num - oldm.jit_trace_num)
print("trace_abort = " .. newm.jit_trace_abort - oldm.jit_trace_abort)
end
f()
Результат: trace_num = 1
, trace_abort = 0
. Отлично.
А в следующей функции, похоже, есть код, способный вызвать у LuaJIT проблемы:
jit.opt.start(0, "hotloop=2", "hotexit=2", "minstitch=15")
_G.globalthing = 5
function f()
jit.flush()
collectgarbage("collect")
local oldm = misc.getmetrics()
collectgarbage("collect")
local sum = 0
for i = 1, box.space._vindex:count()+ _G.globalthing do
box.execute([[SELECT RANDOMBLOB(0);]])
require('buffer').ibuf()
_G.globalthing = _G.globalthing - 1
end
local newm = misc.getmetrics()
print("trace_num = " .. newm.jit_trace_num - oldm.jit_trace_num)
print("trace_abort = " .. newm.jit_trace_abort - oldm.jit_trace_abort)
end
f()
Результат: trace_num
— от 2
до 4
, trace_abort = 1
. Это означает, что нужно было сгенерировать до четырех трассировок вместо одной, причем что-то заставило LuaJIT прекратить попытки. Дальнейшая трассировка показывает, что проблема не в подозрительно выглядящих операторах внутри функции, а в вызове jit.opt.start
. (Содержимое файла jit.dump
может помочь разобраться, как протекала компиляция трасс.)
Пример с jit_snap_restore и деоптимизацией производительности
Если кривая наклона метрики jit_snap_restore
после изменений в старом коде растет, это может означать, что LuaJIT чаще останавливает выполнение кода на трассах. А это, в свою очередь, может указывать на снижение производительности.
Рассмотрим следующий код:
function f()
local function foo(i)
return i <= 5 and i or tostring(i)
end
-- параметр minstitch нужен, чтобы эмулировать поведение non-stitching
jit.opt.start(0, "hotloop=2", "hotexit=2", "minstitch=15")
local sum = 0
local oldm = misc.getmetrics()
for i = 1, 10 do
sum = sum + foo(i)
end
local newm = misc.getmetrics()
local diff = newm.jit_snap_restore - oldm.jit_snap_restore
print("diff = " .. diff)
end
f()
Результат: diff = 3
, поскольку при выполнении трассы возможны три сторонних выхода. Один находится в конце цикла. Другие два переключают выполнение на интерпретатор, прежде чем LuaJIT решит, что фрагмент кода «горячий» (значение параметра hotloop
по умолчанию равно 56
согласно документации LuaJIT).
А теперь изменим единственную строку в функции local foo
, чтобы получить следующий код:
function f()
local function foo(i)
-- функция math.fmod еще не скомпилирована
return i <= 5 and i or math.fmod(i, 11)
end
-- параметр minstitch нужен, чтобы эмулировать поведение non-stitching
jit.opt.start(0, "hotloop=2", "hotexit=2", "minstitch=15")
local sum = 0
local oldm = misc.getmetrics()
for i = 1, 10 do
sum = sum + foo(i)
end
local newm = misc.getmetrics()
local diff = newm.jit_snap_restore - oldm.jit_snap_restore
print("diff = " .. diff)
end
f()
Результат: значение diff
увеличилось, так как сторонних выходов стало больше. Этот тест показывает, что измение кода влияет на производительность.
Если кривая наклона метрики jit_snap_restore
после изменений в старом коде растет, это может означать, что LuaJIT чаще останавливает выполнение кода на трассах. А это, в свою очередь, может указывать на снижение производительности.
Рассмотрим следующий код:
function f()
local function foo(i)
return i <= 5 and i or tostring(i)
end
-- параметр minstitch нужен, чтобы эмулировать поведение non-stitching
jit.opt.start(0, "hotloop=2", "hotexit=2", "minstitch=15")
local sum = 0
local oldm = misc.getmetrics()
for i = 1, 10 do
sum = sum + foo(i)
end
local newm = misc.getmetrics()
local diff = newm.jit_snap_restore - oldm.jit_snap_restore
print("diff = " .. diff)
end
f()
Результат: diff = 3
, поскольку при выполнении трассы возможны три сторонних выхода. Один находится в конце цикла. Другие два переключают выполнение на интерпретатор, прежде чем LuaJIT решит, что фрагмент кода «горячий» (значение параметра hotloop
по умолчанию равно 56
согласно документации LuaJIT).
А теперь изменим единственную строку в функции local foo
, чтобы получить следующий код:
function f()
local function foo(i)
-- функция math.fmod еще не скомпилирована
return i <= 5 and i or math.fmod(i, 11)
end
-- параметр minstitch нужен, чтобы эмулировать поведение non-stitching
jit.opt.start(0, "hotloop=2", "hotexit=2", "minstitch=15")
local sum = 0
local oldm = misc.getmetrics()
for i = 1, 10 do
sum = sum + foo(i)
end
local newm = misc.getmetrics()
local diff = newm.jit_snap_restore - oldm.jit_snap_restore
print("diff = " .. diff)
end
f()
Результат: значение diff
увеличилось, так как сторонних выходов стало больше. Этот тест показывает, что измение кода влияет на производительность.