Оптимизация в Lua
Перевод статьи Brent Sorrentino на русский язык.
Сегодняшний урок посвящен оптимизации производительности, теме, которая интересует всех разработчиков. Некоторые из этих советов будут очевидны, другие, нет. Главное правило - необходимо соотносить затрата на оптимизацию с выгодой, получаемое в результате. Если оптимизация может дать 2% прирост производительности, но его реализация требует 50 часов дополнительного кодирования, то особого смысла тратить время нет. Однако, если 10 часов кодирования способны дать заметные улучшения производительности программы, установленной у массы пользователей, то задача является необходима.
Большинство трюков производительности, представленных в этом руководстве, относятся в первую очередь к "критичных по времени" процедурам - там где уменьшение времени исполнения положительно влияет на общий результат. Такими процедурами являются, например, блоки длительных вычислений, загрузки кадров и играх и т.п. Ускорение таких процедур качественно улучшает потребительские качества всего продукта.
1. Локализация, Локализация, Локализация...
Независимо от того, сколько раз об этом уже говорилось, стоит подчеркнуть еще раз. Хотя полностью избежать использования глобальных переменных невозможно, минимизация их использования - хорошая практика. Доступ к локальным переменных и функциям просто быстрее.
Неправильно
CCX = display.contentCenterX --global variable for i = 1,100 do local image = display.newImage( "myImage" ) image.x = CCX end
Правильно
local CCX = display.contentCenterX --local variable for i = 1,100 do local image = display.newImage( "myImage" ) image.x = CCX end
Это также относится к стандартным Lua библиотекам. В критичных по времени процедурах вы всегда должны локализовать библиотечные функции.
Неправильно
local function foo( x ) for i = 1,100 do x = x + math.sin(i) end return x end
Правильно
local sin = math.sin -- локальная ссылка на math.sin local function foo(x) for i = 1,100 do x = x + sin(i) end return x end
Лучше принять за правило: функции должны всегда быть локализованы, если это возможно. Конечно, для этого понадобится правильное определение области видимости.
Неправильно
function func1() func2( "myValue" ) end function func2( y ) print( y ) end func1()
Правильно
local function func2( y ) print( y ) end local function func1() func2( "myValue" ) end func1()
2. По возможности избегайте функции в качестве аргументов для других функций
В циклах или критическом по времени коде имеют значния функции, являющиеся параметрами других функций. Изучите эти два случая:
Определение в качестве аргумента другой функции - Неправильно
local func1 = function(a,b,func) return func(a+b) end for i = 1,100 do local x = func1( 1, 2, function(a) return a*2 end ) print( x ) end
Локализованная - Правильно
local func1 = function( a, b, func ) return func( a+b ) end local func2 = function( c ) return c*2 end for i = 1,100 do local x = func1( 1, 2, func2 ) print( x ) end
3. Избегайте table.insert()
Давайте сравним четыре метода, выполняющие одну и ту же задачу: заполнение таблицы значениями. Функция table.insert() дает самые плохие результаты из всех и её стоит избегать.
table.insert()
local a = {} local table_insert = table.insert for i = 1,100 do table_insert( a, i ) end
Метод прямой индексации - Правильно
local a = {} for i = 1,100 do a[i] = i end
Метод с размером таблицы- Допустимо
local a = {} for i = 1,100 do a[#a+1] = i end
Метод счетчика - Правильно
local a = {} local index = 1 for i = 1,100 do a[index] = i index = index+1 end
4. Сведение к минимуму использования unpack()
Функция unpack() не является "скорострельной". Однако в большинстве случаев её можно написать простым и более эффективным циклом.
Метод Lua "unpack()" - Нежелательно
local a = { 100, 200, 300, 400 } for i = 1,100 do print( unpack(a) ) end
Метод цикла - Желательно
local a = { 100, 200, 300, 400 } for i = 1,100 do print( a[1],a[2],a[3],a[4] ) end
Проблема второго метода в том, что он не работает для таблиц неизвестной длины. В остальных случаях лучше использовать именно его.
5. Кэширование доступа к элементам
Кэширование таблиц, особенно внутри циклов, также несколько повышает производительность.
Номера для кэширования - Допустимо
for i = 1,100 do for n = 1,100 do a[n].x = a[n].x + 1 print( a[n].x ) end end
Сохраненная копия - Правильно
for i = 1,100 do for n = 1,100 do local y = a[n] y.x = y.x + 1 print( y.x ) end end
6. Избегайте ipairs()
Если результат можно достичь средствами языка Lua, не стоит использовать ipairs()
ipairs () - Неправильно
local t1 = {} local t2 = {} local t3 = {} local t4 = {} local a = { t1, t2, t3, t4 } for i,v in ipairs( a ) do print( i,v ) end
Правильно
local t1 = {} local t2 = {} local t3 = {} local t4 = {} local a = { t1, t2, t3, t4 } for i = 1,#a do print( a[i] ) end
7. Производительность математических функций
Некоторые математические функции и процессы быстрее других. Стоит использовать более быстрые функции.
Избегайте math.fmod() для положительных чисел
-- Метод math.fmod local fmod = math.fmod for i = 1,100 do if ( fmod( i,30 ) < 1 ) then local x = 1 end end
Метод остатка от деления (рекомендуется)
-- Метод остатка от деления for i = 1,100 do if ( ( i%30 ) < 1 ) then local x = 1 end end
Умножение происходит быстрее, чем деление
x * 0.5 ; x * 0.125 -- Правильно x/2 ; x/8 -- Неправильно
Умножение происходит быстрее, чем возведение в степень
x * x * x -- Правильно
x^3 -- Неправильно
10. Загрузка больших данных вне критических фрагментов
Все данные, которые используются в критических по времени моментах, должны быть подготовлены заранее. Например, файлы настроек или звуковые эффекты. Подходящими моментами для первых является момент старта программы, для вторых - момент перед началом сцены или уровня. При желании, звуковые эффекты могут быть организованы в виде таблицы для удобства оперирования ими и уничтожения в момент, когда они больше не нужны.
-- Загрузка звуков во время некритического по времени кода
local soundTable = { mySound1 = audio.loadSound( "a.wav" ), mySound2 = audio.loadSound( "b.wav" ), mySound3 = audio.loadSound( "c.wav" ), mySound4 = audio.loadSound( "d.wav" ), mySound5 = audio.loadSound( "e.wav" ), mySound6 = audio.loadSound( "f.wav" ), mySound7 = audio.loadSound( "g.wav" ), mySound8 = audio.loadSound( "h.wav" ), }
В момент воспроизведения достаточно
local mySound = audio.play( soundTable["mySound1"] )
Естественно, не стоит забывать уничтожать объекты, когда они больше не нужны, очищая ссылку из таблицы:
local ST = soundTable for s,v in pairs(ST) do audio.dispose( ST[s] )
ST[s] = nil end