« 开发笔记(28) : 重构优化 | 返回首页 | 开发笔记(29) : agent 跨机 id 同步问题 »

Lua 字节码与字符串的共享

我们的系统的应用场合比较特殊,在同一个进程内存在数千个 lua_State

Lua 的虚拟机占用的内存已经足够小了,但还是抗不住数量多啊。所以我希望有版本节约一些内存。

最想做的一件事情是把不同 lua_State 中相同的函数字节码合并起来共用一块内存。要做到这一点并不复杂。而且可以提高一些内存访问的效率。(因为大部分 lua 程序在并行执行相同的逻辑)

首先我们需要准备一个用来共享数据块的模块,它必须是线程安全的。因为既然分到了不同的 lua_State 就是想利用并发的优势。针对这个特定需求定制这样一个模块可以做到 lock-free 。

进程开始时初始化一个足够大的 hash 表,不要管 hash 冲突的问题,每个节点只需要初始化一次,这样可以最大可能的简化逻辑。就不必用锁了。

对每个可能需要共享的内存块,我们可以先计算 hash 值,然后比对 hash 表中是否有相同 hash 值的节点,然后做一次全文比较。如果相同,就可以直接引用以前的数据块。

如果已经存在相同 hash 值但内容不同的数据块,那么不要替换掉旧数据块(用 CAS 指令可以做到这点)。

在数据块上做一个标记区分这两种数据块,当释放数据块的时候,在 hash 表中的永远不要释放,不在 hash 表中的没有多个引用就可以释放了。

由于 hash 表中的值只可能为空,或是一个有唯一意义的值。且一旦被赋值后,就不可能更改(引用的内存也永不释放)。这个机制就不需要锁来保证并发安全了。由于 hash 表的大小有限,这套机制所占用的总内存也不可能无限增加,所以是可控的。


剩下个工作就是修改 lua 的虚拟机实现。我在 lobject.h 中给 Proto 结构增加了一个域标记其 opcode 是否被共享化。然后在 lparser.c 中,找到 close_func 这个函数,再构造完 opcode 的 buffer 后(可以同样处理的还有 lineinfo 这组调试信息),把指针传入前面的模块,并从 Lua 管理的内存中释放掉。最后设置标记表示 opcode 已经被复制到全局共享区。

在 lfunc.c 中的 luaF_freeproto 可以找到释放代码。当检查到共享标记时,改为通知共享模块去处理。

如果 lua 代码不是通过 parser 加载,那么还需要修改 lundump.c


可以被共享的数据必须符合的原则是,一旦够构造出来,就不会被修改。符合这个原则的数据还有 lua 中的字符串 TString 。

但我们需要先的 TString 做一点小改造。lua 直接把字符串内存附加在 TString 这样一个 GCObject 的内存后面。这会使得不同 lua_State 中的字符串继续完全相同,结构也会有差异。

简单的改法是给 TString 加一个指针,指向独立的内存块存放字符串内容。只需要修改 lobject.h 中的 getstr(ts) 宏就可以改变 lua 代码中所有访问字符串体的方式。注:这会损失一丁点性能。

然后修改 lstring.c 中的 createstrobj 函数,lstring.h 中的sizestring(s) 宏,以及 lgc.c 中的 freeobj 函数就大功告成了。


我在我们的系统上做了对比测试,平均每个 lua_State 可以节省下 500K 左右的内存,大部分是在 opcode 上节约下来的。这也和我们每个 lua 虚拟机中加载的代码量接近,证明这个方法是有效且被正确实现了。

我们每个 'lua_State` 占用 5 到 10 M 内存。所以大约节约了 10% 左右的内存,不算特别可观。以后还要在实际环境中评测。

考虑到日后一台主机上会有至少 2000 个 lua 虚拟机在运行,我认为省下 1G 的内存应该还是有意义的。尤其是这些是运行 lua 字节码最常访问到的部分,对 CPU cache 的利用很有好处。当然在现阶段只是臆测,过段时间再做细致的测试比较。


对有耐心读完的读者,这里放一个彩蛋 :)

最近有一点闲情,所以继续写了那本书两章。对,就是那本传说中的《Lua 源码欣赏》。上次提到它都是今年一月份的事情了。

依旧不保证它不会太监掉。我写起来才发现工程巨大啊,感觉至少还有 80% 的内容要写。为了让书中的细节不出错,需要反复校对源代码,比之前几次阅读 lua 的代码累多了。

开始动笔时,lua 还是 5.2.0 版,现在已经是 5.2.1 了,改动的位置不少。相比最初完整阅读 Lua 代码时的 Lua 5.0 以及后来再读时的 Lua 5.1 ,每次阅读都是新体验。

希望在 Lua 5.3 发布前能够完成。

Comments

正在学习中,或许下个版本就能参加讨论了。。。

拜读了!

感觉有点深奥,看完了一头雾水!没有基础!先去入入门,再来学习博主的文章!

云风,你的博客太受欢迎了,我觉得原因在于“干货”比较多,大家能通过这个学到东西,thankswww.tyebh120.com

很好非常好,希望能越来越好!

云风,看过你之前说对GO很欣赏,为何不用它来开发你现在做的东西呢?是因为你要用Lua嵌入到C里么?

云风大哥早些完成lua源码欣赏, 应该有很多人会买这本书来读的。 真期待啊。

我擦, 终于看到一本lua的源码解析的书籍了

最近在做一个lua项目,学习了

1.5节

过渡设计->过度设计

写的太好了,学习学习!

@sw

有 coroutine 的支持, 完全不需要有对应数量的 os thread.

正如向 os 申请很多线程, 内核线程也没有实际上看起来的多一样.

skynet 做的就是这个事情.

恩,买早餐的路上想了想,的确是这个需求的话,云风的做法是对Lua源代码或者概念改动最小的……
2000的数量级?看来是打算一个连接一个State了……而且共用这么多,也可以说明是一份代码多个线程跑……可是2000个线程……有必要么,poll模型不好么,我个人认为只有需要耗时操作时,才需要线程做强制的抢占。其实可以做个poll模型,每个State通过类select方式轮转。耗时操作集中起来,以服务的方式提供(在lua内部依然按照异步yield的方式处理),除了耗时操作外不用线程,这样是不是好一点呢……

不错~~

这其实是个矛盾。lua_State完全独立,而coroutine无法并发。那么既需要并发又需要共享某些数据(无论是为了逻辑还是性能)就尴尬了。

一种思路是State共享数据(共享常数据从而不打破代码逻辑);一种则是考虑让coroutine能并发。

总觉得这里应该有一个能融合两种不同类型的抽象层,也许可以做的更漂亮一点。我得想想~

mark & reading ~ :)

@CatIsFlying
牛人喜欢自己造轮子。用云风大神的话说,Erlang这种还是太重量了。核心代码都是自己的理解和优化起来更好控制。看看skynet的设计确实不错。

太高级了,完全看不懂。不过在进程内起那么多的虚拟机,应该是想实现基于环境隔离和消息的并发吧。如果是的话,为什么不采用Erlang之类的现成解决方案?

最近迷上lua,非常适合我们这种c程序员用,期待《lua源码欣赏》的完成!

Post a comment

非这个主题相关的留言请到:留言本