lua性能优化之luajit官方指南和补充注解

时间:2022-11-18 06:00:15

编写目的:luajit是速度最快的脚本语言之一,但是在用的时候发现并没有达到官方宣称的那么快,官方也因此给了一些luajit在支持jit模式下中编写lua代码的指南,地址大家可以参照http://wiki.luajit.org/Numerical-Computing-Performance-Guide。但是官方只给了怎么去做,并没有给出为什么这么去做,以及做了后会发生什么,提高多少性能。所以本文就是在官方给的指南基础上给出一些注解,让大家知道来龙去脉。

1.Reduce number of unbiased/unpredictable branches(减少不可预测的分支代码):因为luajit采用的是trace compiler的工作方式,会将经常使用的字节码按照预先的假设编译成机器码,并为此生成一段guard守护代码来做快速调用机器码时候的判定。如果字节码不可预测经常发生变化,那么luajit就会跳出守护代码并生成新的机器码以及对应的guard守护代码。这样一来就会出现性能上面的下降,所以我们应该尽量让代码做到可预测性,在if else这样的语句中应该将大概率的条件放在前面,这样在执行时,luajit就尽最大可能生成一段守护代码和机器码就完成95%以上分支代码调用。

2.use ffi data structures(如果可以,将你的数据结构用ffi实现,而不是用lua table实现):这是因为lua table本质上是一种hash table,除了分配基础数据成员外还会分配很多额外的数据信息(如:原表,索引表等),而ffi可以做到只分配基础数据成员。所以ffi实现的数据结构自然比lua table实现的数据结构内存占用要低的多。而且在访问时jit会利用ffi的信息来直接内存访问基础数据成员,而lua table却要走一次hask key操作。所以访问速度ffi实现的数据结构自然也比lua table实现的数据结构要快的多。测试代码如下:

local ffi = require("ffi")
ffi.cdef[[
typedef struct { float x, y, z; } vector3c;
]]

local count = 100000

local function test1() -- lua table的代码
local vecs = {}
for i = 1, count do
vecs[i] = {x=1, y = 2, z = 3}
end
local total = 0
-- gc后记录下面for循环运行时的时间和内存占用,这里省略
for i = 1, count do
total = total + vecs[i].x + vecs[i].y + vecs[i].z
end
end
local function test2() -- ffi的代码
local vecs = ffi.new("vector3c[?]", count)
for i = 1, count do
vecs[i] = {x=1, y = 2, z = 3}
end
local total = 0
-- gc后记录下面for循环运行时的时间和内存占用,这里省略
for i = 1, count do
total = total + vecs[i].x + vecs[i].y + vecs[i].z
end
end

3.call c functions only via the ffi(尽可能用ffi来调用c函数):表面上看是省略掉像tolua这样的第三方工具做导出的麻烦,实际上是带来了质的性能提升。因为将extern c对应的c函数交给ffi调用时,需要向ffi中注入该c函数的申明,拿到了具体的形式参数和返回参数后,luajit就能对该c函数做jit优化操作,从而提高访问该c函数的性能。

4.find the right balance for unrolling(循环展开,有利有弊,需要自己去平衡):循环展开通常作为减少循环次数的一种方式,过多的循环展开反而会被luajit取消展开,所以需要使用者自己做测试来获取循环展开临界点,做出适当的展开处理。测试代码如下:

// c函数中循环展开的一种例子,用来减少循环次数,此处仅做循环展开术语解释
void Init_Array(int *dest, int n)
{
int i;
int limit = n - 3;
for(i = 0; i < limit; i+= 4)//每次迭代处理4个元素
{
dest[i] = 0;
dest[i + 1] = 0;
dest[i + 2] = 0;
dest[i + 3] = 0;
}
for(; i < n; i++)//将剩余未处理的元素再依次初始化
dest[i] = 0;
}

5.use plain ‘for i=start,stop,step do … end’ loops(实现循环时,最好使用简单的for i = start, stop, step do这样的写法,或者使用ipairs,而尽量避免使用for k,v in pairs(x) do):这是因为直接使用数值索引不仅更有利于循环的展开,而且也可以被jit优化来提高性能。而k,v方式在luajit的2.1.0中是不支持jit的,坊间流传是k,v方式jit优化处理时对应的汇编代码难写。

6.define and call only ‘local’ (!) functions within a module(其他模块中的函数应该使用local缓存下来进行调用):这样可以在多次调用其他模块函数时,只需从其他模块中一次寻址函数的操作,从而提高查询效率。

7.cache often-used functions from other modules in upvalues(其他模块中的函数使用local缓存下来时存在upvalues中):寻找该函数时只需在upvalues对象内查找,而无需从该函数所在的模块内寻找,从而提高查询效率。

8.avoid inventing your own dispatch mechanisms(避免使用你自己实现的分发调用机制,而尽量使用內建的例如metatable这样的机制):这是因为直接使用table或者metatable表查找可以被luajit进行jit优化处理。而自己实现的分发机制往往会用到分支代码或者其他更复杂的代码结构。这些都是不容易被jit优化处理的,性能上反而不如纯粹的表查找+jit优化来得快。测试代码如下:

编程的时候为了结构优雅,常常会引入像消息分发这样的机制,然后在消息来的时候根据我们给消息定义的枚举来调用对应的实现,过去我们也习惯写成:
if opcode == OP_1 then
elesif opcode == OP_2 then
...
但在luajit下,更建议将上面实现成table或者metatable
local callbacks = {}
callbacks[OP_1] = function() ... end
callbacks[OP_2] = function() ... end

9.do not try to second-guess the jit compiler(无需过多去帮jit编译器做手工优化):对jit不是很熟练的情况下不要尝试对jit进行手工优化,有时反而适得其反。比如:z = x[a+b] + y[a+b],这在luajit是性能ok的写法,不需要先local c = a+b然后z = x[c] + y[c]。因为local变量越多,寄存器占用越大,当寄存器不足时反而会jit失败。

10.be careful with aliasing, esp. when using multiple arrays(变量的别名可能会阻止jit优化掉子表达式,尤其是在使用多个数组的时候):因为数组变量别名是引用类型,不能随便改变引用指向,否则取值可能出现问题,所以jit可能会取消这种情况下的优化。示例代码如下:

x[i] = a[i] + c[i]; y[i] = a[i] + d[i]
我们可能会认为两a[i]是同一个东西,编译器可以优化成
local t = a[i]; x[i] = t + c[i]; y[i] = t + d[i]
实则不然,因为可能会出现,x和a就是同一个表,这样,x[i] = a[i] + c[i]就改变了a[i]的值,那么y[i] = a[i] + d[i]就不能再使用之前的a[i]的值了

11.reduce the number of live temporary variables(减少存活着的临时变量的数量):因为临时变量存放在寄存器中,而寄存器不足时jit会失败。所以我们不应该大量使用local并且避免local变量深度过大而一直存活着,可以使用do end来包含临时变量,严控临时变量生命周期,降低寄存器数量占用峰值,提高jit成功率。

12.do not intersperse expensive or uncompiled operations(减少使用高消耗或者不支持jit的操作):不支持jit的字节码或者内部库调用都应该尽量不用,比如常见的字符串拼接,for pairs遍历以及NYI(作者未实现jit的部分,可以参照http://wiki.luajit.org/NYI)。