« skynet 对客户端广播的方案 | 返回首页 | 使用 MPTCP 增加对 github 的带宽 »

为什么 Lua 的新版本越来越慢?

今天有人转了个知乎上的帖子给我看:Lua 5.3为何很慢?该不该升级?

首先,我不认为 10% 的性能差异能够称的上很大,和 10% 的性能下降相比,程序更清晰稳定、功能更完备(不是指功能多,而是指对各种边界条件处理的更好)要重要的多。毕竟,让 CPU 提升 10% 的性能很容易。

其次,在实际项目中,和简单的测试脚本不同,我很难观察到 10% 的差异。(我们的服务器用过 lua 5.2 和 lua 5.3 两个版本,很难从线上压力上感知到性能差别)。

如果你真的用那些简单的测试脚本做一个比较,lua 5.1 比它的前一个版本 lua 5.0 要慢得多。差别或许比 lua 5.1 到 5.3 还要大。而为什么很少人关心这个,去用回 5.0 呢?说到底还是因为 luajit 导致的 lua 社区的分裂,让 lua 5.1 这个中间版本变成了另一个 lua 而已。

我的硬盘上一直留有从 lua 4.0 开始几乎每个 lua 小版本的源代码副本。而近十年来,一直都在跟进 lua 的源码变迁,所以我对 lua 的每次修改都或多或少有一些印象。下面谈谈我对这个新版本越来越慢的一点个人看法吧。

这写这篇 blog 前,我花了一天做了大量的测试对比工作。包括做profile ,编译出汇编逐行对比,阅读版本间的 diff 等等。因为不是写 paper ,懒得把这些工作整理在 blog 里了。主要写写结论。


我认为,lua 小版本演化在一些简单测试脚本跑分中,性能 5% ~ 10% 的下降,主要是更健壮的实现的代价。也就是考虑到各种边界条件,逐渐增加的判断。以及一些不完备的功能在完善过程中增加的必要代码。另外,还包括了 gc 模块的不断改进。

比如,一开始的版本( 5.1 )不能在 pcall 中 yield ,不能在 hook 里 yield ;而后来的版本抹去了这些限制。同时,设计和实现都复杂了一些。但如果你仔细阅读代码,你会发现这些改动都不是简单的补丁,而精心考虑过的重设计,实现上都几乎无可挑剔。单从局部代码的来看,你都很难找到哪里是导致性能下降的热点。

再比如,有人怀疑 lua 5.3 把 number 类型在内部拆分成整数和浮点两个子类型会导致多余的判断和分支。这会造成一些性能下降。但其实增加子类型本身是几乎没有额外的性能开销的(尤其是最常见的 table index 操作)。你可以对比前后版本的实现,看看到底是否真的有多余的分支。

对比 lua 5.3 和 5.2 工作量很大,我们可以找个更简单的地方来看:

其实给数据类型区分子类型并不是从 5.3 开始的,甚至不是某个大版本变更。实现的变化发生在 lua 5.2.0 到 5.2.1 之间。在这个小版本升级的过程中,lua 利用 type 字段的高位开始为数据类型区分内部子类型。

在 lua 5.2.1 中,字符串被分为短字符串和长字符串两类。只有短字符串才做内部化(和之前版本的字符串行为一致),而长字符串则额外处理。

lua 5.2.1 和 5.2.0 的 diff 文件不大,只有 2000 行左右。而且一半都是周边库的实现变化。所以慢慢阅读几百行 diff 花上个把小时大家都能做了。

如果你写一些简单的测试脚本,你会发现很多情况下 lua 5.2.1 的确和 lua 5.2.0 有性能差异 1% 上下浮动。或快或慢和具体脚本怎么写有关。

但这个差异真的和 string 的实现有关吗?

我专门把和 string 有关的代码从 lua 5.2.1 patch 到 lua 5.2.0 上做了测试(而保留了其它的差异),发现性能差异几乎消失了。同时,我仔细查看了实现部分编译生成的汇编部分,在主要流程上基本是完全一致的。

而性能差异的部分,一部分来至 gc 部分的改写。而另一方面,lua function 和 c function 可以更清晰的用子类型区分开(而之前是在 function 结构中保留了一个 boolean 变量),代码清爽了很多,对于某些测试脚本,甚至比之前的版本跑的还要快一点点。


为什么很多简单的测试脚本却能感知到明显的差异呢?

这是因为,很多脚本使用了 for 循环去重复测试简单的函数。而 lua 5.3 在 for 数字循环中,需要多做一次子类型判断。对循环变量为整数和浮点数分别处理;而之前的版本是不需要的。

别小看这多一次判断,如果你的 for 循环中做的工作本身很简单,那么差异很可能被放大到 1% 的量级。但实际我们的项目中,却很难让 for 循环本身大比例占据 cpu 时间的。

至于四则运算,之前的版本同样要做类型判断。只要你不在 lua 5.3 中混用整数和浮点,那么甚至还会比老版本更快一些(尤其在不支持硬件浮点运算单元的时候)。

另一方面,由于 lua 5.3 原生支持了 64bit 整数以及新增加的语言级的位操作,对于特定领域来说,却可以对性能做更大幅度的加分。


有些变化的确会影响到性能。比如 lua 5.2 开始取消了 C 层面的对象环境的概念,改成了 _ENV 。这相对于 lua 5.1 ,api 和实现都得到了精简;同时,全局变量这个东西连同对应的 OPCODE 都去掉了,读取全局变量变成了访问 _ENV 这个 upvalue ,性能也收到了轻微了影响。

比如在最前面提到的知乎的帖子中,测试脚本在内循环里访问了 math.abs 函数。而 math 是一个全局变量。如果你能在脚本最前面加一行 local math = math 的话,你会发现 lua 5.1 和 lua 5.2 几乎没有区别。

至于 lua 5.3 的性能轻微下降,我个人认为是 for 循环的差异导致的。


lua 历代版本的源代码可以在这里下载 ,如果你对我上面的结论有异议,可以自己每个版本去对比。

btw, 在历史上,lua 5 对 lua 4 那次大版本的升级,几乎重新实现了一遍。而性能是得到了很大的提高的。是不是我们该期待 lua 6 的再次重构呢 :)

Comments

为了验证您是人类,请将六加一的结果(阿拉伯数字七)填写在下面:

我是外星人咋办?

lua现在快沦为最慢的脚本语言了,看看我的测试:
http://blog.csdn.net/llj1985/article/details/50750313

@ichenq:

用C怎么了,重点是代码简洁易懂

@ichenq:

用C怎么了,重点是代码简洁易懂

说的太高深了 看的我云里雾里

多谢解答。

今天正好看到有篇关于提升Lua代码性能的文章http://wuzhiwei.net/lua_performance/
其实绝大多数时候,都能从算法和数据结构上提升性能,而且Luajit了之后效率会更快

这种问题或许根本不值得研究一天,因为单次“版本升级”和“性能提升”首先没有因果关系。bugfix最容易引起性能下降。其次,几乎没有人单纯依靠版本升级来提升系统性能的。知乎问该不该升级,我认为兼容性才是首先要考虑的问题。

@咙想酒甜
skynet的代码模块性很好的,只是没有你熟悉的class而已

魂淡,因为你那个skynet不支持win下的VS,又是纯C写的,一堆全局变量看上去真是丑,我们技术总奸叫我把skynet转换成win下的C++代码....

@叶小桃

这种问题没必要问,能够抛开历史包袱进行彻底的升级,也是Lua受欢迎的原因之一

上个月跟Roberto Ierusalimschy聊了一下,不过没有问关于以后会不会有新版本的问题。他自称是“不是很仁慈的独裁者”,他认为Lua的用户大多数会固定使用一个版本的Lua,所以他也不怕因为升级而导致的不兼容问题。但是,对于为什么现在C语言都已经有intptr_t了而Lua还不愿意使用tagged reference,是考虑到很多嵌入式设备上连C99编译器都还没有,所以Lua也要兼顾这些应用。

话说,Lua解释器的性能相比Python什么的脚本语言已经很不错了(虽然比起编译的还是很慢),10%的话,如果相比原本就很慢的解释执行的话,只是稍微更慢了那么一点点。但毕竟Lua历史上最早是为数据录入以及系统扩展而设计的,并不是为了榨出处理器的最后一滴点性能。如果真是要这种简单计算程序的性能,总觉得应该换工具,比如LuaJIT或者Terra,或者去为编程语言做点贡献。

Post a comment

非这个主题相关的留言请到:留言本