Что я могу сделать, чтобы повысить производительность программы Lua? - PullRequest
24 голосов
/ 30 сентября 2008

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

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

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

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

Ответы [ 5 ]

58 голосов
/ 12 октября 2012

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

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

Итак, когда вы пишете что-то на 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 фактически занимает больше времени, чем тело цикла, поэтому результаты профилирования могут быть искажены.)

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

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. Несмотря на то, что это нарушает прозрачность, производительность в кэшированных значениях улучшается из-за одного меньшего вызова функции.

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

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

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

Частичное применение

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

-- 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);
-- ...

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

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

Вот более обширный ("реальный мир") пример с некоторыми пропусками кода, в противном случае он легко занял бы всю страницу здесь (а именно 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));

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

9 голосов
/ 29 ноября 2008

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

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

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

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

local strfind = string.find

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

4 голосов
/ 28 ноября 2008
  • Создание наиболее часто используемых локальных функций
  • Эффективное использование таблиц в качестве HashSets
  • Снижение создания таблицы путем повторного использования
  • Использование luajit!
2 голосов
/ 26 июня 2017

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

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

2 голосов
/ 28 ноября 2008

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...