最近有一个有趣的发现,调整了一行 Lua 代码的顺序,执行时间却少了接近一半 😅segmentfault
状况下面这个 lua 脚本 order-1.lua
:函数
local function f2 (...) return select('#', ...) end local function f1 (...) local l = select('#', ...) local m = 0 for i = 1, l do m = m + select(i, ...) end local n = f2(...) return m + n end local n = 0 for i = 1, 1000 * 1000 * 100 do n = n + f1(1, 2, 3, 4, 5) end print("n: ", n)
执行时间为 6.3s
:性能
$ time luajit order-1.lua n: 2000000000 real 0m6.343s user 0m6.342s sys 0m0.000s
若是将其中的 f1
函数实现,调整一下顺序:学习
local function f1 (...) local n = f2(...) local l = select('#', ...) local m = 0 for i = 1, l do m = m + select(i, ...) end return m + n end
这个改动是将 n
的计算放到 m
计算的前面。
从逻辑上来讲,m
和 n
两个是并无顺序依赖,先算哪个都同样的,可是执行时间却少了将近一半:lua
$ time luajit order-2.lua n: 2000000000 real 0m3.314s user 0m3.312s sys 0m0.002s
首先确定不是什么诡异问题,计算机但是人类最真实的伙伴了,哈哈 😄日志
此次是 Lua 这种高级语言,也不是 上次那种 CPU 指令级 的影响了。code
此次是由于 LuaJIT 的 tracing JIT 技术的影响。ci
不像 Java 那种 method based JIT 技术,是按照函数来即时编译的。LuaJIT 是按照 trace 来即时编译的,trace 对应的是一串代码执行路径。
LuaJIT 会把热的代码路径直接即时编译生成机器码,一串热的代码路径也就是一个 trace。同时 trace 也不是无限长的,LuaJIT 有一套机制来控制 trace 的开始结束(之后找时间再详细记录一篇的)。get
具体来讲是这样子的,由于在 order-1.lua
里,TRACE 1
在 m
计算的那个 for 循环处则中止了,当 TRACE 2
开始的时候,LuaJIT 还不支持这种状况下即时编译 (还处于 NYI 状态)VARG
这个字节码(也就是对应的 ...
)。it
因此,致使了这部分代码不能被 JIT,回归到了 interpreter 模式,因此致使了这么大的性能差别。
以下,咱们能够在 LuaJIT 输的日志中看到 NYI: bytecode 71
这个关键信息。
$ luajit -jdump=bitmsr order-1.lua ... ---- TRACE 2 start 1/3 order.lua:13 0016 UGET 2 0 ; f2 (order.lua:13) 0017 VARG 4 0 0 (order.lua:13) ---- TRACE 2 abort order.lua:13 -- NYI: bytecode 71
调整了 Lua 代码顺序,影响了 LuaJIT 中 trace 的生成,致使了有字节码无法被 JIT,这部分回退到了解释模式,从而致使了较大的性能差别。
JIT 技术仍是蛮好玩,不过须要学习掌握的东西也挺多的。
以我目前的理解,tracing JIT 算是很牛的 JIT 技术了,有其明显的优点。不过任何一项技术,老是少不了很是多的人力投入。即便像 Lua 这种小巧的语言,也仍是有很多的 NYI 没有被 JIT 技术。像 Java 这种重型语言,JIT 这方面的技术,怕是须要不少大牛才堆出来的。