Lua 中具有大量用户数据的有效垃圾回收

Effective garbage collection in Lua with large userdata

本文关键字:有效 数据 用户数 用户 Lua      更新时间:2023-10-16

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

    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

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

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

有没有办法对特定对象或仅对用户数据运行垃圾回收?

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

但是你可以做很多事情来改善这种情况:

。我很快就用完了内存,因为垃圾收集器跟不上。

GC

跟踪完整用户数据的大小(并相应地增加 GC 债务),但不知道轻用户数据的大小。 如果你 lua_pushlightuserdata ( L, ptr );在其他地方分配的值(通过mallocmmap等),GC "看到"大小为零。 如果使用 lua_newuserdata ( L, size ) 进行分配,GC 将知道完整大小。

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

如果做不到这一点,请在每次分配之前插入显式 GC 步骤调用(使用 $((以 KB 为单位的不可见分配大小))步骤,以模拟使用完整用户数据的大部分内容),并使用 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 个分配减少到两个分配。 如果以这种方式定义它们,一个不错的技巧是,您还可以使用与运算符元方法相同的函数。(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

您添加了足够的延迟,以便可以内联编写表达式(当值实际标记为 free 时,如果两者之间没有其他发生任何操作,则将计算二进制操作):

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])会中断。

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