Lua 性能优化实录-如何把 Lua 对象打印加速13倍

37 阅读4分钟

Lua 性能优化实录-如何把 Lua 对象打印加速13倍

打印对象内容是排查问题的必须功能,不管是调试代码还是线上日志,都需要知道对象背后到底是啥。

Lua的对象类型很少,我们几乎只用考虑Table该怎么打印,但是这里面的坑比想象的多:

  1. 引用陷阱:A 指向 B,B 又指回 A,如果不做防环检测,结果就是栈溢出。
  2. 数据冗余:同一个 Table 被多处引用,不能老老实实打出来,要标个指针ID表明它们是同一个对象。
  3. 嵌套层次: Table内存允许下可以有巨多嵌套,但我们不能无限深入,得给个最大层次。
  4. 格式要求:要么紧凑利于日志输出,要么有层次关系便于阅读,字段名最好也符合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

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++ 的痛苦在于“写得慢”,但回报是“跑得飞快”。

这本质上是一个开发效率与运行效率的博弈:

  1. 对于底层的模块,需求变化少,写一次用大半辈子那种,值得掉一些头发换取极致性能。。
  2. 对于上层逻辑但性能敏感,不提速负载上不来那种,比如战斗运算视野管理,值得掉更多头发用C/C++重写。
  3. 其他的一切以开发效率为王,少敲几行代码,少思考内存有没有释放意味着你能写更多功能,同时有更多时间运动健身娱乐,多陪陪家人。

5.代码呢?

你也许会问:说了这么多,代码在哪里?Talk is cheap, show me the code

此刻它还属于我的“私有库”,正在整理中,请保持关注。