有时因为业务需要,要将一个 Lua Table 序列化,如果不在乎序列化结果的可读性,可以直接将 Table 序列化成二进制数据块,如果需要结果的可读性,可以将 Table 序列化成字符串。
要序列化的内容
因为 Lua 的 Table 中可以保存各种各样的内容,不可能都序列化,而且基本也不会有这种需求,比如像是 Function 这种变量,序列化出来并无意义,因为在另一个环境下反序列化以后就不能用了。
出于这个原因, Function 和 Thread 变量不管是 key 还是 value 都不会处理。如果 key 是 Table 变量,则也不处理,同时 Table 中不能有对其它 Table 的循环引用。
虽然看起来有一些限制,但是因为序列化 Table 一般都是用在保存或者发送上,所以其实本身就基本上不会有这种受限制的需求。
二进制
将 Table 序列化为二进制数据块的速度是最快的,体积也是最小的,如果没有可读性要求,且序列化远比反序列化的次数多,那就果断选择序列化成二进制。
云风大佬有一个现成的实现可以直接拿来用 lua-serialize,自己实现一个也不难,就是使用 Lua 提供的 C Api 遍历 Table,然后根据 key 和 value 的类型,不断写入到一个可以动态扩容的内存块链表中,全部写入完成以后再将链表中的所有块拷贝进一个连续内存中。
除了结果没有可读性以外,序列化成二进制还有一个缺点就是结果要反序列化成 Table 的时候依然需要逆向解析一遍。
字符串
序列化成字符串的优点是,结果可读性好,而且不需要额外实现反序列化,可以直接使用 load 来加载字符串实现反序列化。
Lua 实现
用 Lua 来实现是最顺畅最简单的,不用写 C 代码,也不需要编译。如果没有高频使用的话,用 Lua 序列化就足够用了。
有一个非常好的第三方实现可以用来解决这个问题,serpent,功能强大,不仅可以序列化,还可以将 Table 以可读方式打印出来。
C/C++ 实现
由于工作需要高频将 Table 序列化成字符串,且单个 Table 数据量也不少。所以一直想找一个 C/C++ 语言的实现版本,但是 github 上找了半天也没找到。后来周末抽了个时间自己实现了一个 luaseri-cpp。
实现并不复杂,但是碰到了一个问题,就是 snprintf 在处理 double 的时候非常慢。搜索以后发现了一个非常切合我需求的项目,dtoa-benchmark,是腾讯的大佬叶劲峰实现的,学 C++ 的应该都听说过他,《C++ Primer 5th》中文版的审校之一。该项目给出了他对 Grisu2 算法的实现,效率是挺好的,就直接拿来用了。
性能对比
对比以上的三种实现,经过测试,可以得出性能的大致对比结果。
在序列化一个大型 Table 的时候,以 lua-serialize 的用时为基准 n,luaseri-cpp 的用时为 1.8n - 2n,serpent 的用时大概为 40n 左右。lua-serialize 序列化出来的结果也比其余两种结果的长度小很多,具体比例与 Table 中的数据有关系,当 Table 中的数字越多时,长度差距越大。
反序列化的时候,lua-serialize 需要使用它自带的反序列化函数,同样以它的反序列化事件为基准 m,序列化为字符串的实现使用 Lua 自带的 load 函数加载字符串得到 Table,使用的时间大概在 2m 左右。
可以看到 lua-serialize 无论在序列化和反序列化上时间都占优不少,在空间使用上优势更大。所以如果不需要结果的可读性的话,直接使用它即可。如果需要可读性,那么使用 luaseri-cpp 的效率也是比较快的。如果不喜欢用 C 库,或者对性能没要求的情况下可以使用 serpent 来实现。
感觉可以在开发版本序列化成字符串,方便调试,在线上版本使用 lua-serialize,效率和空间都能占优不少。