lua 中 40 字节以下的字符串会被内部化到一张表中,这张表挂在 global state 结构下。对于短字符串,相同的串在同一虚拟机上只会存在一份。
在 skynet 中,有大量的 lua vm ,它们很可能加载同一份 lua 代码。所以我之前改造过一次 lua 虚拟机,[让它们可以共享 Proto] 。这样可以加快多个虚拟机初始化的速度,并减少一些内存占用。
但是,共享 Proto 仅仅只完成了一半的工作。因为一段 lua 代码有一很大一部分包含了很多字符串常量。而这些常量是无法通过共享 Proto 完成的。之前的方案是在 clone function 的时候复制一份字符串常量。
或许,我们还可以做的更进一步。只需要让所有的 lua vm 共享一张短字符串表。
首先要考虑的是并发冲突的问题。
如果我们使用开散列 hash 表,预先分配好大量的队列(比如 128K 个),那么就可以只针对 slot 加读写锁。鉴于大量短字符串都是在加载代码的时候构建的,同样的代码不需要构建两次。所以这个读写锁的写频率会非常的低,性能应该不会有什么问题。
比较麻烦的如何回收那些不再使用的对象。
由于共用同一份 TString 对象,所以不能简单的为每个字符串记数(每个引用它的 vm 加一)。我最初的想法是永远不再释放那些短字符串,因为看起来总量并不多。
但在公司里讨论以后,很多人还是很担心。如果程序要运行数个星期,很难保证不会产生大量的垃圾短字符串。大多数是运行时拼接的临时字符串。
我想了一个折中的方案。默认情况下,新构造的 lua vm 是不回收任何短字符串的。但是一旦要求它开始收集,它会在 gc mark 的过程,给短字符串做一个标记表示我正在使用。我在一个全局变量里设置了一个版本号,这个版本号将作为标记设入 TString 对象。
一旦一个短字符串被标记过长期保留,它就不能被改为需要回收(打上版本标记)。
所有在共享 Proto 过程中产生的短字符串(他们的总数是可以被预估的,有限的),都将打上永不回收的标记。而 skynet 运行时,通过 lua 服务模块启动的 lua vm 在运作时,将随 gc mark 流程给短字符串打上版本标记。
一旦我们需要对短字符串池做清理。我们可以先递增一下全局版本号,然后通知 skynet 的 launcher 服务,通知所有活动的 lua 服务做一次完整的 gc 。这可以保证在记录的全局版本号之后,所有活动的 lua vm 都 mark 过它们正在使用的短字符串。
最后,就可以安全的把那些版本号更小的短字符串清理掉。
有兴趣的同学可以在 github 上取下 sstring 分支看一下这个 patch 是否能给你的项目带来好处:
可能的好处包括,减少每个 agent 的内存使用,以及加快 agent 的启动速度。
ps. 记得这个 patch 修改了 lua 的实现,所以更新代码后,需要先运行一次 make cleanall 保证 lua 库能重新编译链接。