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
正在学习中,或许下个版本就能参加讨论了。。。
Posted by: PigFly | (18) March 19, 2014 06:03 PM
拜读了!
Posted by: aozima | (17) November 28, 2013 12:13 AM
感觉有点深奥,看完了一头雾水!没有基础!先去入入门,再来学习博主的文章!
Posted by: Anonymous | (16) December 3, 2012 11:01 AM
云风,你的博客太受欢迎了,我觉得原因在于“干货”比较多,大家能通过这个学到东西,thankswww.tyebh120.com
Posted by: Anonymous | (15) November 22, 2012 03:53 PM
很好非常好,希望能越来越好!
Posted by: 五金配件 | (14) November 21, 2012 10:08 AM
云风,看过你之前说对GO很欣赏,为何不用它来开发你现在做的东西呢?是因为你要用Lua嵌入到C里么?
Posted by: lpk | (13) November 16, 2012 12:11 AM
云风大哥早些完成lua源码欣赏, 应该有很多人会买这本书来读的。 真期待啊。
Posted by: ctx | (12) November 15, 2012 04:12 PM
我擦, 终于看到一本lua的源码解析的书籍了
Posted by: ctx | (11) November 15, 2012 04:06 PM
最近在做一个lua项目,学习了
Posted by: Jack | (10) November 14, 2012 11:13 AM
1.5节
过渡设计->过度设计
Posted by: shuax | (9) November 11, 2012 09:27 AM
写的太好了,学习学习!
Posted by: 五金配件 | (8) November 10, 2012 02:31 PM
@sw
有 coroutine 的支持, 完全不需要有对应数量的 os thread.
正如向 os 申请很多线程, 内核线程也没有实际上看起来的多一样.
skynet 做的就是这个事情.
Posted by: Cloud | (7) November 10, 2012 02:17 PM
恩,买早餐的路上想了想,的确是这个需求的话,云风的做法是对Lua源代码或者概念改动最小的……
2000的数量级?看来是打算一个连接一个State了……而且共用这么多,也可以说明是一份代码多个线程跑……可是2000个线程……有必要么,poll模型不好么,我个人认为只有需要耗时操作时,才需要线程做强制的抢占。其实可以做个poll模型,每个State通过类select方式轮转。耗时操作集中起来,以服务的方式提供(在lua内部依然按照异步yield的方式处理),除了耗时操作外不用线程,这样是不是好一点呢……
Posted by: sw | (6) November 10, 2012 09:30 AM
不错~~
这其实是个矛盾。lua_State完全独立,而coroutine无法并发。那么既需要并发又需要共享某些数据(无论是为了逻辑还是性能)就尴尬了。
一种思路是State共享数据(共享常数据从而不打破代码逻辑);一种则是考虑让coroutine能并发。
总觉得这里应该有一个能融合两种不同类型的抽象层,也许可以做的更漂亮一点。我得想想~
Posted by: sw | (5) November 10, 2012 07:42 AM
mark & reading ~ :)
Posted by: xishvai | (4) November 10, 2012 12:46 AM
@CatIsFlying
牛人喜欢自己造轮子。用云风大神的话说,Erlang这种还是太重量了。核心代码都是自己的理解和优化起来更好控制。看看skynet的设计确实不错。
Posted by: Genius | (3) November 9, 2012 08:40 PM
太高级了,完全看不懂。不过在进程内起那么多的虚拟机,应该是想实现基于环境隔离和消息的并发吧。如果是的话,为什么不采用Erlang之类的现成解决方案?
Posted by: CatIsFlying | (2) November 9, 2012 07:24 PM
最近迷上lua,非常适合我们这种c程序员用,期待《lua源码欣赏》的完成!
Posted by: rongekuta | (1) November 9, 2012 05:45 PM