设置元 table:引用与内联的优势?

Setting a meta table: Advantage of reference vs inline?

我想知道当您想使用相同的元 table 时,通过引用传递元 table 与在 setmetatable() 中在线声明它是否有意义多个 tables.
我的目标是节省内存,但前提是它真的有很大的不同。

我说的是这个:

-- Passing the meta table by reference: 
JSON1 = {
    metaTable = {
        __index = function (t, k)
            -- ...
        end;
        __call = function()
            -- ...
        end
    };
    parse = function(filePath)
        local fakeParsedJson = {}
        setmetatable(fakeParsedJson, JSON1.metaTable) -- Right here
        return fakeParsedJson(filePath)
    end;
}

VS

-- Passing the table in-line:
JSON2 = {
    parse = function(filePath)
        local fakeParsedJson = {}
        setmetatable(fakeParsedJson, { -- Right here:
            __index = function (t, k)
                -- ...
            end;
            __call = function()
                -- ...
            end
        })
        return fakeParsedJson(filePath)
    end;
}

我试图找出内存使用情况是否存在显着差异,但我能找到的唯一方法是比较 gcinfo:

local start1 = gcinfo()
local example2_1 = JSON2.parse('example2_1.json')
local example2_2 = JSON2.parse('example2_2.json')
local example2_3 = JSON2.parse('example2_3.json')
local example2_4 = JSON2.parse('example2_4.json')
local example2_5 = JSON2.parse('example2_5.json')
print(gcinfo()-start1) -- Prints 1

local start2 = gcinfo()
local example1_1 = JSON1.parse('example1_1.json')
local example1_2 = JSON1.parse('example1_2.json')
local example1_3 = JSON1.parse('example1_3.json')
local example1_4 = JSON1.parse('example1_4.json')
local example1_5 = JSON1.parse('example1_5.json')
print(gcinfo()-start2) -- Prints 1

这是我的 fiddle:https://repl.it/HfwS/34

看起来确实没有什么区别。但我只是不知道引擎盖下到底发生了什么。

当您调用 setmetatable(myTable,myMetaTable) 时,会将 myMetaTable 的完整副本写入 myTable 中的某处,还是仅存储一个简单的引用?因为如果它只是存储一个引用,那么让我所有的 table 指向同一个元 table.

就很有意义了

(在 x86_64 上,在 Lua 5.3 中)每个(空)table 占用 56 个字节。 table 中的每个 key/value 条目占用 32 个字节(但条目数四舍五入为下一个 2 的幂)。 (不同 versions/platforms 的字节数可能不同,但大致相同 +/- 2 左右的幂。)

如果您在 metatable 中有两个条目,则每个 metatable 有 120 个字节。 (您还创建了闭包 (function() … end),因此实际上可能更多。)

将 table 构造函数置于调用 setmetatable 的参数位置意味着 每次执行该调用时,都会创建一个新的独立 table(+ functions 的新闭包,...)。 (另请参阅参考手册中的 the section on table constructors。)没有智能编译器/没有重复数据删除/……事实上,不可能,因为其他代码可能(可能)修改元table ,然后在单个共享 metatable 和一个 metatable 之间存在明显的语义/可观察差异。如果这不是很明显,比较

Foo = { __name = "Foo", dump = print } ; Foo.__index = Foo
function newFoo( )  return setmetatable( { }, Foo )  end

function newFoo( )
    local mt = { __name = "Foo", dump = print }
    mt.__index = mt
    return setmetatable( { }, mt )
end

如果你说

t = { newFoo( ), newFoo( ), newFoo( ) }
getmetatable( t[1] ).dump = function( self )  print "<Foo>"  end
for _, v in ipairs( t ) do  v:dump( )  end

第一个版本将打印

<Foo>
<Foo>
<Foo>

而第二个将打印(例如)

<Foo>
Foo: 0x1267010
Foo: 0x1267120

这是明显不同的行为。所以编译器/... 不能删除相同的元tables,因为其他代码(尚未看到)可能修改其中一个 metatable,然后观察到的行为就会不同。

▶ 这意味着如果您创建多个 (meta)tables,它们必须保存在某个地方。存储多个 tables 必然比存储一个 table 使用更多的内存,因此在调用 setmetatable 的参数位置有一个 table 构造函数将比创建一个 table 使用更多的内存] 首先,然后在调用中传递对它的引用。


也就是说,担心内存使用 不应该是您主要关心的问题 。代码的语义/"meaning"/可观察到的行为更为重要。

  • 如果您修改元table,所有 "objects"/值的行为是否应该改变?或者你想通过元table标识(getmetatable( x ) == Foo)来确定对象类型?然后你必须使用共享元table(或等效结构)。
  • 如果修改元table,是否应该只改变一个 "object" 的行为?然后你必须构建并使用每个"object"/值的单独元table。
  • 只有当您知道您永远不会修改元table时,才不会比较元table引用来确定对象类型,不会... ,那么这些不同的方法将表现出相同的外部可见行为,只有这样你才能根据次要关注点(如内存使用、便利性/代码简洁性等)自由选择。

(一般来说,需要单独修改的 metatables 非常 很少见,所以使用共享的 metatables (首先创建并传递引用setmetatable) 是通常的方法——它节省内存并且更利于调试。)


旁白:gcinfo 非常 旧且仅 returns 所用内存量的整数近似值。请改用 collectgarbage "count",然后您会看到不同之处。 (它使用了 returns 千字节,所以乘以 1024 得到字节。)