Подтвердить что ты не робот

Что я могу сделать для повышения производительности программы Lua?

Я задал вопрос о Lua perfromance, а в ответ спросил:

Вы изучили общие советы по поддержанию высокой производительности Lua? т.е. знать создание таблиц и скорее использовать таблицу, чем создавать новую, использовать "local print = print" и т.д., чтобы избежать глобальных доступов.

Это немного другой вопрос из Lua Patterns, Tips and Tricks, потому что я бы хотел получить ответы, которые особенно влияют на производительность и (если возможно) объяснение того, почему производительность оказывает влияние.

Один совет на каждый ответ был бы идеальным.

4b9b3361

Ответ 1

В ответ на некоторые другие ответы и комментарии:

Верно, что в качестве программиста вы, как правило, избегаете преждевременной оптимизации. Но. Это не так для языков сценариев, где компилятор не оптимизирует много - или вообще.

Итак, всякий раз, когда вы пишете что-то в Lua, и это выполняется очень часто, выполняется в критичной по времени среде или может работать некоторое время, хорошо знать, что нужно избегать (и избегать).

Это коллекция того, что я узнал с течением времени. Некоторые из них я узнал по сети, но, будучи подозрительным, когда речь идет о сетях, я сам сам тестировал все это. Кроме того, я прочитал документ об оценке Lua на Lua.org.

Некоторая ссылка:

Избегать глобальных привязок

Это один из самых распространенных советов, но заявить, что он еще раз не повредит.

Глобалы хранятся в хэш-таблице по их имени. Доступ к ним означает, что вам нужно получить доступ к индексу таблицы. Хотя у Lua довольно хорошая реализация хэш-таблицы, она все еще намного медленнее, чем доступ к локальной переменной. Если вам нужно использовать глобальные переменные, присвойте их значение локальной переменной, это будет быстрее при доступе второй переменной.

do
  x = gFoo + gFoo;
end
do -- this actually performs better.
  local lFoo = gFoo;
  x = lFoo + lFoo;
end

(Не то, что простое тестирование может дать разные результаты, например, local x; for i=1, 1000 do x=i; end здесь заголовок for loop занимает больше времени, чем тело цикла, поэтому результаты профилирования могут быть искажены.)

Избегайте создания строк

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

Популярным вариантом избежать чрезмерного создания строки является использование таблиц. Например, если вам нужно собрать длинную строку, создайте таблицу, поместите туда отдельные строки и затем используйте table.concat, чтобы присоединиться к ней один раз

-- do NOT do something like this
local ret = "";
for i=1, C do
  ret = ret..foo();
end

Если foo() вернет только символ A, этот цикл создаст серию строк, таких как "", "A", "AA", "AAA" и т.д. Каждая строка будет хэшироваться и находиться в памяти, пока приложение не завершится - см. проблему здесь?

-- this is a lot faster
local ret = {};
for i=1, C do
  ret[#ret+1] = foo();
end
ret = table.concat(ret);

Этот метод вообще не создает строки во время цикла, строка создается в функции foo, и в таблицу копируются только ссылки. Впоследствии concat создает вторую строку "AAAAAA..." (в зависимости от того, насколько велика C). Обратите внимание, что вы можете использовать i вместо #ret+1, но часто у вас нет такого полезного цикла, и вы не будете использовать переменную итератора, которую вы можете использовать.

Другим трюком, который я нашел где-то на lua-users.org, является использование gsub, если вам нужно проанализировать строку

some_string:gsub(".", function(m)
  return "A";
end);

Сначала это выглядит странно, преимущество в том, что gsub создает строку "сразу" в C, которая только хэшируется после того, как она возвращается обратно в lua при возврате gsub. Это позволяет избежать создания таблицы, но, возможно, имеет больше служебных накладных расходов (если вы не вызываете foo() в любом случае, но если foo() на самом деле является выражением)

Избегайте накладных расходов

Использовать языковые конструкции вместо функций, где это возможно

Функция ipairs

При повторной обработке таблицы служебные данные функции из ipairs не оправдывают ее использование. Для итерации таблицы используйте

for k=1, #tbl do local v = tbl[k];

Он делает то же самое без накладных вызовов функции (пары фактически возвращают другую функцию, которая затем вызывается для каждого элемента таблицы, а #tbl оценивается только один раз). Это намного быстрее, даже если вам нужно значение. И если вы не...

Примечание для Lua 5.2. В 5.2 вы можете фактически определить поле __ipairs в метатете, что делает использование ipairs полезным в некоторых случаях. Тем не менее, Lua 5.2 также делает поле __len для таблиц, поэтому вы, возможно, предпочтете приведенный выше код ipairs, поскольку метатет __len вызывается только один раз, тогда как для ipairs вы получите дополнительную функцию вызов на итерацию.

функции table.insert, table.remove

Простое использование table.insert и table.remove может быть заменено вместо этого с помощью оператора #. В основном это для простых операций push и pop. Вот несколько примеров:

table.insert(foo, bar);
-- does the same as
foo[#foo+1] = bar;

local x = table.remove(foo);
-- does the same as
local x = foo[#foo];
foo[#foo] = nil;

Для сдвигов (например, table.remove(foo, 1)), и если заканчивается разреженная таблица, нежелательно, конечно, лучше использовать функции таблицы.

Использовать таблицы для SQL-IN сравнивает

Вы могли бы или не могли бы принимать решения в своем коде, например, следующие

if a == "C" or a == "D" or a == "E" or a == "F" then
   ...
end

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

local compares = { C = true, D = true, E = true, F = true };
if compares[a] then
   ...
end

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

Избегать создания частых таблиц

Это подробно обсуждается в Lua Performance Tips. В основном проблема заключается в том, что Lua выделяет вашу таблицу по требованию, и поэтому ее выполнение на самом деле занимает больше времени, чем очистка содержимого и повторное заполнение.

Однако это немного проблема, поскольку сам Lua не предоставляет метод для удаления всех элементов из таблицы, а pairs() не является самим производителем. Я еще не проводил тестирование производительности по этой проблеме.

Если вы можете, определите функцию C, которая очищает таблицу, это должно быть хорошим решением для повторного использования таблицы.

Избегайте делать то же самое более и более

Это самая большая проблема, я думаю. Хотя компилятор на не интерпретируемом языке может легко оптимизировать много избытков, Lua не будет.

Memoize

Использование таблиц в Lua можно сделать довольно легко. Для функций с одним аргументом вы даже можете заменить их таблицей и метаданом __index. Несмотря на то, что это разрушает прозрачность, производительность лучше кэшированных значений из-за одного меньшего вызова функции.

Вот реализация memoization для одного аргумента с использованием метатетируемого. (Важно: этот вариант не поддерживает аргумент значения nil, но довольно неплохо для существующих значений.)

function tmemoize(func)
    return setmetatable({}, {
        __index = function(self, k)
            local v = func(k);
            self[k] = v
            return v;
        end
    });
end
-- usage (does not support nil values!)
local mf = tmemoize(myfunc);
local v  = mf[x];

Фактически вы можете изменить этот шаблон для нескольких входных значений

Частичное приложение

Идея похожа на memoization, которая заключается в "кешировании" результатов. Но вместо кэширования результатов функции вы должны кэшировать промежуточные значения, поместив их вычисления в функцию-конструктор, которая определяет функцию вычисления в этом блоке. На самом деле я бы просто назвал это умным использованием закрытий.

-- Normal function
function foo(a, b, x)
    return cheaper_expression(expensive_expression(a,b), x);
end
-- foo(a,b,x1);
-- foo(a,b,x2);
-- ...

-- Partial application
function foo(a, b)
    local C = expensive_expression(a,b);
    return function(x)
        return cheaper_expression(C, x);
    end
end
-- local f = foo(a,b);
-- f(x1);
-- f(x2);
-- ...

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

Крайним вариантом этого будет Currying, но на самом деле это скорее способ имитировать функциональное программирование, чем что-либо еще.

Вот более обширный пример ( "реальный мир" ) с некоторыми упущениями кода, иначе он бы легко занял всю страницу здесь (а именно get_color_values действительно выполняет большую проверку значений и распознает принимаемые смешанные значения)

function LinearColorBlender(col_from, col_to)
    local cfr, cfg, cfb, cfa = get_color_values(col_from);
    local ctr, ctg, ctb, cta = get_color_values(col_to);
    local cdr, cdg, cdb, cda = ctr-cfr, ctg-cfg, ctb-cfb, cta-cfa;
    if not cfr or not ctr then
        error("One of given arguments is not a color.");
    end

    return function(pos)
        if type(pos) ~= "number" then
            error("arg1 (pos) must be in range 0..1");
        end
        if pos < 0 then pos = 0; end;
        if pos > 1 then pos = 1; end;
        return cfr + cdr*pos, cfg + cdg*pos, cfb + cdb*pos, cfa + cda*pos;
    end
end
-- Call 
local blender = LinearColorBlender({1,1,1,1},{0,0,0,1});
object:SetColor(blender(0.1));
object:SetColor(blender(0.3));
object:SetColor(blender(0.7));

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

Ответ 2

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

Первый закон оптимизации: не надо.

Мне бы хотелось увидеть проблему, когда у вас есть выбор между ipairs и парами и можно измерить эффект разницы.

Один простой кусочек низко висящих фруктов - это помнить о том, чтобы использовать локальные переменные в каждом модуле. Это вообще не стоит делать такие вещи, как

local strfind = string.find

если вы не можете найти измерение, говорящее вам иначе.

Ответ 3

  • Выполнение наиболее часто используемых функций locals
  • Использование таблиц как HashSets
  • Уменьшение создания таблицы путем повторного использования
  • Использование luajit!

Ответ 4

Держите таблицы короткими, чем больше таблица, тем дольше время поиска. И в той же строке, итерации по числовым индексированным таблицам (= массивы) быстрее, чем таблицы на основе клавиш (таким образом, ipairs быстрее, чем пары)

Ответ 5

Следует также указать, что использование полей массива из таблиц намного быстрее, чем использование таблиц с любым типом ключа. Случается (почти), что все реализации Lua (включая LuaJ) хранят вызванную "часть массива" внутри таблиц, к которой обращаются поля массива таблицы, и не сохраняет ключ поля или не ищет его;).

Вы даже можете имитировать статические аспекты других языков, таких как struct, С++/Java class и т.д. Локали и массивы достаточно.