Lua中有效的垃圾收集,包含大量用户数据

时间:2016-07-15 08:46:54

标签: c++ c lua garbage-collection user-data

我已经在Lua(C API)之上实现了一个解决量子力学问题的代码。它为脚本语言增加了量子力学算子和波函数。到现在为止还挺好。挑战在于wavefunction userdata可能很大(userdata包含指向1Mb和1Gb之间数组的指针)我已经添加了标准的垃圾收集方法,对于简单的情况,它们可以工作。

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

进行

和metamethod调用

    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

psi是几个(10-100)波函数的表(数组),因为垃圾收集器无法跟上,所以我的内存耗尽很快。

更糟糕的是,我已经注册了数千个字符串和常量(数字),因此完整的垃圾收集需要经历许多变量

有没有办法在特定对象或用户数据上运行垃圾收集?

1 个答案:

答案 0 :(得分:2)

不,目前不是(< = 5.3.x)。

但是你可以采取一些措施来改善这种状况:

  

...由于垃圾收集器无法跟上,我的内存很快耗尽。

GC跟踪完整用户数据的大小(并相应地增加GC债务),但是对于轻用户数据并不知道它们。如果你 lua_pushlightuserdata ( L, ptr );在其他位置(mallocmmap等)分配的值,GC"看到"大小为零。如果您使用lua_newuserdata ( L, size )进行分配,则GC会知道完整大小。

你可能会使用lua_newuserdata作为一个瘦的包装器结构(以获得__gc)" fat"从该结构引用的数据(不可见地为Lua),因此当您占用千兆字节时,GC会看到几千字节的已用内存。如果可能,尝试将内部事物的分配器从malloc切换到lua_newuserdata。 (除非您需要跨Lua状态共享数据和/或需要特殊对齐或有其他约束......)这应该解决大多数问题。 (它仍将不断收集东西,但至少它不再是OOM。)

如果失败,请在每次分配之前插入显式GC步骤调用(使用$((以千字节为单位的不可见分配的大小)步骤,模拟您使用完整用户数据的大部分内容),并使用GC步骤乘数和/或暂停。

如果这还不够,你可以尝试更多涉及丑化代码的事情。

运算符元方法只知道这两个参数,因此它们总是必须创建一个新的目标值。如果使用函数而不是运算符,则可以将现有的废弃值作为目标传递,例如

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

其中qaddqsubqmul等需要(a,b[, target])并重新使用target如果给定或以其他方式分配新存储空间。 (这会将N ^ 2分配的循环减少到两个分配。)如果你这样定义它们的一个很好的技巧是你也可以使用与operator metamethods相同的函数。 (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,并且只有在返回nil时才实际分配新值。你的代码必须改变,例如:

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

......看起来更好并且不需要跟踪中间值,但是很容易因为意外地过早释放而中断(例如,而不是在最后释放psi[j],执行psi[j] = _(psi[j]) - _(_(psi[i] * psi[j]) * psi[j])会打破。)

虽然这最后一个变体(差不多)恢复了看似正常的表情,但它真的不是一个好主意。之前的那个并不比使用函数而不是运算符好,但需要更多的脆弱簿记而且速度更慢。因此,如果您决定对分配进行微观管理,我建议您使用第一个版本的变体。 (您也可以使用函数作为方法,可能使用像火炬一样切换的参数(target:add(a,b))(但是您需要在代码中明确分配target并且需要先知道大小操作...)所有这些变化都以不同的方式吮吸。尽量避免走这么远,并且失败建立你最不喜欢的那个。)