2016-07-15 2 views
2

Я реализовал код для решения задач квантовой механики поверх Lua (C API). Он добавляет квантовомеханические операторы и волновые функции к языку сценария. Все идет нормально. Задача состоит в том, что пользовательские данные wavefunction могут быть большими (Userdata содержит указатели на массивы от 1Mb до 1Gb). Я добавил стандартные методы сбора мусора и для простых случаев, в которых они работают.Эффективная сборка мусора в Lua с большими userdata

static int LuaWavefunctionDestroy(lua_State * L) 
    { 
     WaveFunctionType *psi = (WaveFunctionType*) luaL_checkudata(L, 1, "Wavefunction_Type"); 
     WaveFunctionFree(psi); 
     return 0; 
    } 

и Метаметод вызовы с использованием _gc

static const struct luaL_Reg Wavefunction_meta[] = { 
     {"__add", LuaWavefunctionAdd}, 
     {"__sub", LuaWavefunctionSub}, 
     {"__mul", LuaWavefunctionMul}, 
     {"__div", LuaWavefunctionDiv}, 
     {"__unm", LuaWavefunctionUnm}, 
     {"__index", LuaWavefunctionIndex}, 
     {"__newindex", LuaWavefunctionNewIndex}, 
     {"__tostring", LuaWavefunctionToString}, 
     {"__gc", LuaWavefunctionDestroy}, 
     {NULL, NULL} 
    }; 

Если я однако теперь запустить следующий код в Lua

for j=1,N do 
     for i=j,N do 
     psi[j] = psi[j] - (psi[i] * psi[j]) * psi[j] 
     end 
    end 

с пси быть таблица (массив) несколько (10- 100) Волновые функции У меня быстро заканчивается память, так как сборщик мусора не может идти в ногу.

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

Есть ли способ запустить сбор мусора на конкретном объекте или на только userdata?

+2

Наверное, нет. Но вы можете добавить функцию 'destroy' * * в свои userdatas, которая освобождает данные раньше. – immibis

+1

Я думаю, что вы должны называть сборщик мусора постепенно, когда вы идете, вместо того, чтобы пытаться сделать все это одним выстрелом. Например, проверьте все параметры http://www.lua.org/manual/5.3/manual.html#pdf-collectgarbage. Я бы попытался использовать опцию «шаг» с различными значениями и посмотреть, могу ли я получить хорошие результаты –

ответ

2

Нет, нет в настоящее время (< = 5.3.x).

Но есть куча вещей, которые вы можете сделать, чтобы улучшить ситуацию:

... Я бегу из памяти быстро, как сборщик мусора не может угнаться.

GC отслеживает размеры полных пользовательских данных (и соответственно увеличивает задолженность GC), но не знает их для легких пользовательских данных. Если вы lua_pushlightuserdata(L, ptr); значение, выделенное в другом месте (по malloc, mmap и т. Д.), GC «видит» размер нуля. Если вы используете lua_newuserdata(L, size) для выделения, GC знает полный размер.

Вы, вероятно, использовать lua_newuserdata для тонкой оберточной структуры (для получения __gc) вокруг данных «тучные» ссылочных (невидимо Lua) из этой структуры, соответственно GC видит несколько килобайт используемой памяти, пока вы коробления гигабайты. Если возможно, попробуйте переключить распределитель на внутреннюю вещь от malloc до lua_newuserdata. (Если вам не нужно делиться данными через состояния Lua и/или нуждаться в специальном выравнивании или иметь другие ограничения ...) Это должно решить большинство проблем. (Он все равно будет постоянно собирать материал, но по крайней мере он больше не будет OOM.)

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


Если этого недостаточно, вы можете попробовать более сложные вещи, которые требуют uglifying кода.

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

local psi_ij, psi_ijj 
for j=1,N do 
    for i=j,N do 
    psi_ij = qmul(psi[i],psi[j], psi_ij) 
    psi_ijj = qmul(psi_ij,psi[j], psi_ijj) 
    psi[j] = qsub(psi[j],psi_ijj, psi[j]) 
    end 
end 

где qadd, qsub, qmul и т.д. бы (a,b[, target]) и повторное использование target если дано или иным образом выделить новое хранилище. (Это уменьшит цикл из распределений N^2 до двух распределений.) Хороший трюк, если вы определяете их таким образом, заключается в том, что вы также можете использовать те же функции, что и метамотоды операторов. (target просто всегда будет отсутствовать, так что если вы не заботитесь о распределении вы можете просто использовать операторы.)

(Если psi[k] не все имеют одинаковый размер, так что размеры промежуточных значений меняются, вы можете явно указать target как бесплатную в математической функции, если она несовместима, то, возможно, она будет объединена с хранилищем для поиска, как показано ниже:

Другой вариант заключается в том, чтобы хранить заброшенные значения и явно указывать значения как заброшенные, повторно использовать существующие значения. Поверхностные непроверенные коды:

-- hidden in implementation somewhere 
-- MT to mark per-size stores as weak so GC can collect values 
local weak = { __mode = "k" } 
-- storage of freed values, auto-create per-size storage table 
local freed = setmetatable({ }, { 
    __index = function(t, k) 
    local v = setmetatable({ }, weak) 
    t[k] = v 
    return v 
    end 
}) 

-- interface to that storage 
-- replace size(v) by some way to get a size/layout descriptor 
-- (e.g. number or string giving size in bytes or dimensions or ...) 
function free(v, ...) 
    freed[size(v)][v] = true -- mark as free 
    if ... then return free(...) end 
end 
-- return re-usable value of size/layout vsize, or nil if allocation needed 
function reuse(vsize) 
    local v = next(freed[vsize]) 
    if not v then return nil end 
    freed[vsize][v] = nil -- un-mark 
    return v 
end 

С этим, ваш Распределитель должен тогда сначала проверить reuse и только в том случае, что возвращает ноль фактически выделить новое значение. И ваш код должен быть изменен, как, например:

for j=1,N do 
    for i=j,N do 
    local psi_j = psi[j] 
    local psi_ij = psi[i] * psi_j 
    local psi_ijj = psi_ij * psi_j 
    psi[j] = psi[j] - psi_ijj 
    free(psi_j, psi_ij, psi_ijj) 
    end 
end 

С измененным определением свободного, как

local temp = setmetatable({ }, { __mode = "v" }) 
function free(v) 
    table.insert(temp, v) 
    if #temp > 2 then 
    local w = table.remove(temp, 1) 
    freed[size(w)][w] = true 
    end 
    return v 
end 

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

local _ = free 
for j=1,N do 
    for i=j,N do 
    local psi_j = psi[j] 
    psi[j] = psi_j - _(_(psi[i] * psi_j) * psi_j) 
    free(psi_j) 
    end 
end 

... который выглядит лучше, и избавляется от необходимости отслеживать промежуточные значения, но это пр etty легко сломать, случайно высвободив слишком рано (например, вместо того, чтобы высвобождать psi[j] в конце, делать будет разорвать.)

Хотя этот последний вариант (почти) восстанавливает нормальные выражения, это действительно не такая хорошая идея. И тот, который до этого не намного лучше, чем использование функций вместо операторов, но требует более хрупкой бухгалтерской отчетности и медленнее. Поэтому, если вы решите микроуправление распределениями, я бы предложил использовать вариант в первой версии. (У вас также могут быть функции как методы, возможно, с аргументами, переключаемыми вокруг (target:add(a,b)), как это делает факел (но тогда вам нужно явно выделить target в коде и знать размеры до того, как вы сделаете операцию ...) Все эти вариации сосать по-разному. Постарайтесь не заходить так далеко, и не удается построить тот, который вам не нравится.)

Смежные вопросы