Безресурсные (stateless) итераторы в Lua для оптимизации работы циклов
Опубликовано:
Замыкания требуют динамического расхода памяти, и если их много, это создает нагрузку на сборщик мусора, что может приводить к замедлению работы программы. Посмотрим, как эта проблема решается в Lua через создание так называемых итераторов без состояния, и как они связаны с особенностями работы цикла for...in....
Вернемся к нашему самописному примеру фабрики итераторов (генератору):
local function r_ipairs(t) local index = #t + 1 -- #t - длина массива и последний индекс return function() index = index - 1 if index > 0 then return index, t[index] else return nil end end end local array = {'apple', 'banana', 'pineapple'} for i, v in r_ipairs(array) do print(i, v) end
3 pineapple 2 banana 1 apple
Как мы выяснили ранее при его вызове в памяти создается объект-замыкание, то есть происходит аллокация памяти — ее выделение, в данном случае под саму анонимную функцию и связанные с ней переменные. Когда цикл for...in... проитерирует свое замыкание, так как ссылок на него больше нет, оно станет мусором в памяти.
Встроенный в интерпретатор Lua сборщик мусора рано или поздно очистит от этого объекта память. Однако каждый вызов генератора создает новое замыкание, и если их порождается огромное количество, работа сборщика может тормозить программу. Так в геймдеве, где за игровой кадр может запускаться не один цикл, а в секунде быть по 60 и больше кадров, память будет заполняться быстро. Поэтому в таких случаях используют безресурсные итераторы, а не созданные на основе замыканий.
Безресурсные итераторы по-другому называют бессостоятельными, то есть итераторами без состояния (stateless iterators). В противовес им итераторы на основе замыканий являются состоятельными, то есть итераторами с состоянием (stateful iterators). Хотя безресурсные делают код менее ясным и удобным для программиста, в процессе выполнения программы память динамически на них не выделяется, а значит сборщику мусора не приходится часто работать.
Возможность использования безресурсных итераторов в Lua реализуется через внутренние особенности цикла for...in.... У него есть три скрытые переменные (не имеют отношения к тем, что указываются до in). Одной присваивается ссылка на объект-итератор (итерационную функцию), другой — на таблицу (инвариантные данные), третьей — индекс (меняется).
В тех случаях когда цикл при вызове генератора получает только итератор, двум оставшимся переменным присваивался nil. Их значения игнорируются, ведь итератор с замыканием сам хранит свои состояния и "знает", что возвращать на каждом следующем шаге.
Бессостоятельный итератор ничего в себе не хранит. Это обычная статичная функция в памяти, которая создается в момент запуска программы, а не динамически в процессе выполнения. Генератор, возвращающий такой итератор, на самом деле возвращает не его как объект, а ссылку на него, то есть на функцию. Также генератор возвращает ссылку на таблицу, которую требуется обойти, и стартовый индекс. Если генератор будет вызван множество раз, он столько же раз вернет ссылку на одну и ту же функцию-итератор.
Полученное от генератора цикл for...in... присваивает своим скрытым переменным и далее при каждом проходе и вызове итератора, связанного с одной из своих переменных, передает ему в качестве аргументов ссылку из другой переменной (например, на таблицу) и индекс из еще одной.
Посмотрим, как будет выглядеть наш генератор, если мы его перепишем под безресурсный вариант:
local function r_iter(t, i) i = i - 1 local v = t[i] if v ~= nil then return i, v else return nil end end local function r_ipairs(t) return r_iter, t, #t + 1 end local array = {'apple', 'banana', 'pineapple'} for i, v in r_ipairs(array) do print(i, v) end
3 pineapple 2 banana 1 apple
Значение, которое возвращается из итератора первым, идет и на присвоение первой явной переменной до in и записывается в скрытую переменную цикла (индексную, или управляющую). На следующей проходе эта же переменная передается вторым аргументом при вызове итератора.
Следует отметить, что встроенные функции ipairs и pairs работают по такому же принципу. Они являются фабриками безресурсных итераторов.
Итак, бессостоятельные итераторы в Lua спроектированы исключительно под внутреннюю механику цикла for...in.... Сами по себе, без этого цикла, они практически теряют смысл и неудобны в использовании. Создатели Lua перенесли возможность хранения состояния внутрь самого цикла, чтобы избежать избыточного выделения памяти (ресурсов).
Из-за таких особенностей итерационного цикла в Lua мы не можем следующий генератор, чей итератор с замыканием возвращает только значение, превратить в безресурсный:
local function values(t) local index = 0 return function() index = index + 1 if index <= #t then return t[index] else return nil end end end local array = {'apple', 'banana', 'pineapple'} for i in values(array) do print(i) end
apple banana pineapple
Если мы захотим все же это сделать, stateless-итератор должен будет возвращать индекс. Однако его можно игнорировать. Общепринятым способом является использование нижнего подчеркивания (это полноценная переменная) вместо первой переменной до in:
local function v_iter(t, i) i = i + 1 local v = t[i] if v ~= nil then return i, v else return nil end end local function values(t) return v_iter, t, 0 end local array = {'apple', 'banana', 'pineapple'} for _, v in values(array) do print(v) end
Если из итератора надо вернуть больше чем одно значение помимо индекса, то переменные просто перечисляются после return. Второе, третье, четвертое и все последующие значения по порядку разложатся в ваши пользовательские переменные, указанные до слова in.