序列化 Lua 对象

在项目中由于种种需求经常需要将对象序列化成一个字符串. 由于 Lua 唯一的复合结构是 table, 所以实现起来还是比较简单的. 之前我们的做法是用 Lua 写一个递归函数遍历 table 的键值然后转换成字符串并拼接起来. 然而 Lua 在字符串拼接的过程中会不断地构造字符串对象, 因此这样的实现方式性能较差, 并且会浪费内存, 特别是数据比较大的时候. 一种优化方式是将键值转换的小字符串存到一个 table 里, 最后使用 table.concat 拼接. 不过我想跟进一步, 使用 C 实现它.

C 实现的序列化函数与 Lua 实现的类似, 都是遍历 table, 遇到简单类型就直接转换成字符串, 遇到 table 就递归. 这里我们就可以把键值转换的小字符串存到一个缓冲区里, 然后将缓冲区转换成字符串 push 进栈返回即可. 这比使用 ..table.concat 要快不少. 一开始我直接使用 luaL_Buffer 作缓冲区, 后来发现这玩意儿有坑, 它在扩容的时候往栈里 push 东西, 导致数据不正确. 为此我就自己写一个 buffer 代替 luaL_Buffer. 这里我借鉴了 luaL_Buffer 的设计: 当数据比较小时, 数据直接存在栈里, 不需要向操作系统申请内存; 只有当数据长度超过某个值时才申请内存. 由于项目中大部分序列化操作的数据都比较小, 这个做法可以带来不少优化. 此外我实现的 buffer 实际上是个链表, 扩容的时候只需要追加链表节点即可, 不需要复制数据. 在最后, 只有扩容过的 buffer 才需要将数据复制出来, 否则只需要将头节点数据的指针取出即可.

有时我们对序列化对象的结果没有可读性需求, 这个时候序列化成二进制数据会比较快, 序列化的结果也比较小. 这里我参照了云风大神的项目 cloudwu/lua-serialize. 由于我们的项目没有序列化成 userdata 的需求, 这里我就简化, 并且使用我自己写的 buffer 代替他的 struct write_block. 反序列化时由于不用考虑链表的问题, 也可以简化. 此外我们还需要将序列化的数据在网络中传递, 这就需要考虑整数字节序的问题了. 因此我还加上了字节序转换.

最后粗略测试了下性能, 运行效率大概是 Lua 实现的四倍, 二进制序列化又比文本序列化快一倍以上. 感觉还有优化空间. 完整代码见 luyuhuang/cseri.


序列化 Lua 对象
https://luyuhuang.tech/2020/03/18/serialize-lua-objects.html
Author
Luyu Huang
Posted on
March 18, 2020
Licensed under