今天在公司群里,Net bug 同学提出了一个问题,围绕这个问题大家展开了一系列讨论。讨论中谈及了 lua 中的一个常见的模式:property table ,我觉得挺有意思,记录一下。
最初的问题是:当一个对象的某些属性并不常用,希望做惰性初始化的话,应该怎么实现。
我认为,property table 是一个很符合这个案例的常见模式。
比如,对象 f 有三个可能的成员 a b c ,我们可以不把 f.a f.b f.c 记录在 f 这个 table 里,而是额外有三张大表,a b c 。利用 metatable ,可以在访问 f.a 的时候,实际访问的是 a[f] 。也就是说,所有同类对象的 a 属性,都是从 a 这张表里访问的。
a 这张表的 key 就是对象,value 是对象对应的 a 属性值。
这样做有什么好处?
当对象的运行时用到的属性数量不一时,对象本身占用的空间是固定的。这里的对象变成了带 metatable 的空表,真正的属性值是统一放在有限张大表中的。
大表可以预先分配好内存,而空表占的内存很小且固定,对内存分配器特别友好(容易实现 freelist )。
操作对象本身,不会因为增加属性而造成 rehash 。如果你并不能在对象初始化的时候就决定空间(会改写多少属性),那么好处就会很明显。
另一个明显的好处是,如果你的对象很多,但是有少数对象会设置某种标记或回调函数,然后需要遍历这组特殊对象时,实现方便、性能更高。
我们有一个实际的优化案例:
在心动庄园 的项目中,客户端有大量的对象,只有部分对象带有动画。而带有对象的对象是需要每帧多处理一些事情的。
处理的逻辑之前大致是这样的:
for obj in pairs(objects) do if obj.update then obj:update() end end
在最近一次性能剖析时发现,objects 集合特别大,而需要调用 update 的对象比较少,造成了很大的浪费。所以在优化方案中,我们在设置 update 的时候把对象对象放在另一个集合中,每帧只需要遍历这个较小的集合就够了。
但是,如果一开始我们就用上面提到的 property table 模式,就天然解决了这个问题。因为所有的 update 方法都直接放在了一个集合中了,没有设置这个方法的对象天然就不存在于这个集合。
这里为什么要提到 lua 5.2 后引入的 ephemeron table 这个特性呢?
因为,我们需要把这个 property table 设置为弱 key ,也就是 lua 5.2 文档中所描述的 ephemeron table 。
在 lua 5.1 中,由于没有 ephemeron table 的支持,有可能因为循环引用问题而导致无法回收某些 property 。
例如:如果一个对象有一个属性 ref ,当你设置
a.ref = b b.ref = a
时,会造成循环引用。 ref 这张表里,ref[a] 和 ref[b] 相互引用,导致即使在表外 a b 对象都已不存在了,还是回收不掉。
ephemeron table 解决了循环引用的问题,参考 Eliminating Cycles in Weak Tables 。