Lua 性能优化实录-如何把 Lua 对象打印加速13倍
打印对象内容是排查问题的必须功能,不管是调试代码还是线上日志,都需要知道对象背后到底是啥。
Lua的对象类型很少,我们几乎只用考虑Table该怎么打印,但是这里面的坑比想象的多:
- 引用陷阱:A 指向 B,B 又指回 A,如果不做防环检测,结果就是栈溢出。
- 数据冗余:同一个 Table 被多处引用,不能老老实实打出来,要标个指针ID表明它们是同一个对象。
- 嵌套层次: Table内存允许下可以有巨多嵌套,但我们不能无限深入,得给个最大层次。
- 格式要求:要么紧凑利于日志输出,要么有层次关系便于阅读,字段名最好也符合Lua的样子,能不用["xxx"]的尽量不用。
举个小栗子,下面的Table:
local t = {a=1, b={2,3}, c={d=4, e={5,6}}}
t.c.e[3] = t.c
打印出来是这样子,里面包含了表的引用关系:
{b = {2, 3}, a = 1, c = --[[<table 0x6307dc4fcf30>]]{d = 4, e = {5, 6, <table 0x6307dc4fcf30>}}}
再举个小栗子,下面的Table:
local t2 = {
a = 1,
b = true,
c = {"a", "b", "c"},
d = {x = 1, y = 2, z = 3},
e = 12.278676,
f = {true, true, false, {a='x', b='y'}, ["x-x"] = "x-x", bb='bb', ["324"]=function()end},
}
打印出来是这样子的,字段符合Lua的输出:
{
b = true,
a = 1,
d = {
z = 3,
y = 2,
x = 1
},
c = {
"a",
"b",
"c"
},
f = {
true,
true,
false,
{
b = "y",
a = "x"
},
["x-x"] = "x-x",
["324"] = <function: 0x6307dc4c3ac0>,
bb = "bb"
},
e = 12.278676
}
1. Serpent
Serpent 功能覆盖极其全面,但在高频调用的服务器环境里,它的性能就有点不是那么好看了。
我造了一个比较极端的“压力测试表”:大概 1000 个节点,包含深层嵌套、循环引用、混合 Key 等各种恶心人的情况。
用我自己写的benchmark模块跑分如下:
========================================================================
Name | Ops/Sec | Avg.Op | Total | Ratio
------------------------------------------------------------------------
bench_serpent_line_complex| 1.046 K | 956.18 µs | 0.9562 s | 1x
========================================================================
0.9562 毫秒一次,看起来连1毫秒都不到吗,如果换个说法1秒只能打印1000次,是不是瞬间觉得太拉胯了
2. Lua 简化版
Serpent 是强大,但我只需要80%的功能,也许可以手写了一个精简版的模块,只保留核心的功能。
A few moment later...
写完跑分对比:
=======================================================================
Name | Ops/Sec | Avg.Op | Total | Ratio
-----------------------------------------------------------------------
bench_debugut_str_complex | 1.830 K | 546.31 µs | 0.5463 s | 1x
bench_serpent_line_complex| 1.038 K | 963.47 µs | 0.9635 s | 1.76x
=======================================================================
提升了大概 1.76 倍,虽然快了点,但没发生质变:Lua 的虚拟机开销、大量的临时 String 对象创建、GC 的压力。。。
3. C++ 高性能版
既然 Lua 层面优化不动了,那就 C++ 介入吧。
A long, long time later...
写完这个模块后对着同一个复杂表再测一次:
============================================================================
Name | Ops/Sec | Avg.Op | Total | Ratio
----------------------------------------------------------------------------
bench_objdumper_dump_complex| 14.315 K | 69.86 µs | 0.6986 s | 1.00x
bench_serpent_line_complex | 1.033 K | 967.73 µs | 9.6773 s | 13.85x
============================================================================
哇偶
71 微秒 vs 967 微秒。
比 Lua 版本快了 7.8 倍,比 Serpent 快了 13.8 倍。
复杂表的吞吐量是14315/S属于极端情况,平时我们打印更多的可能是这种两三百字节的Table:
local simple_table = {
uid = 8848,
account = "guest_007",
vip_exp = 1024.5,
history = { "login", "battle", 169999999, false, "logout" },
stats = {
hp = 100, mp = 50, buffs = {},
detail = { crit = 0.5, dodge = 0.1 }
},
bag = {
{ id = 1001, count = 1 },
{ id = 1002, count = 5, locked = true },
},
[100] = "Sparse_Element",
["remote-ip"] = "127.0.0.1"
}
在这种常规场景下,C++ 版本的表现是:
===========================================================================
Name | Ops/Sec | Avg.Op | Total | Ratio
---------------------------------------------------------------------------
bench_objdumper_dump_simple | 315.901 K | 3.17 µs | 0.3166 s | 1.00x
bench_serpent_line_simple | 24.505 K | 40.81 µs | 4.0808 s | 12.89x
============================================================================
来到31.5万每秒:Perfect,Bravo...
4. 总结
写 Lua 简化版只花了A few moments...,Lua 的快乐在于“写得快”,但代价是“跑得慢”。
写 C++ 版本则A long, long time...,伴随着 Segmentation Fault 以及对 Lua 虚拟栈的迷之凝视,C++ 的痛苦在于“写得慢”,但回报是“跑得飞快”。
这本质上是一个开发效率与运行效率的博弈:
- 对于底层的模块,需求变化少,写一次用大半辈子那种,值得掉一些头发换取极致性能。。
- 对于上层逻辑但性能敏感,不提速负载上不来那种,比如战斗运算视野管理,值得掉更多头发用C/C++重写。
- 其他的一切以开发效率为王,少敲几行代码,少思考内存有没有释放意味着你能写更多功能,同时有更多时间运动健身娱乐,多陪陪家人。
5.代码呢?
你也许会问:说了这么多,代码在哪里?Talk is cheap, show me the code
此刻它还属于我的“私有库”,正在整理中,请保持关注。