本文使用对比测评的方式比较了几种Lua中代码和方式,并做了简要的总结和分析。结合其中结论,可指导写出质量更高、性能更优的脚本。本文基于Lua5.1,大部分内容也同样适用于Lua5.2和5.3。本文中引用到的Lua源代码都取自Lua 5.1.5版本。
评测方法
主要有运行时间和产生GC两个指标,以下是获取运行时间和内存用量/GC的方法:
获取运行时间
为了更好地做比较,使用下边的方法来记录代码的运行时间:
1 2 3 4 5 6 7
| local a, b a = os.clock()
b = os.clock() print(b-a)
|
获取内存用量
1 2 3 4 5
| collectgarbage("stop")
collectgarbage("collect")
print(collectgarbage("count"))
|
上边三行代码都是调用函数collectgarbage()
,配合传入的不同的参数达到不同的目的:
stop
停止自动GC
collect
执行一次完整的GC循环
count
返回当前内存用量,以k为单位
使用local保存全局变量的引用
使用local
保存全局变量的引用可以提高变量访问效率。
测试代码
1 2 3 4 5 6 7 8 9 10 11 12
| local sin = math.sin
local a,b a = os.clock() local res = 0 for i = 1, 10000000 do res = res + math.sin(i) end
b = os.clock(); print("cost time = " .. b-a)
|
运行以上代码得到case1和case2的输出分别为:
1 2 3 4 5
| //case 1 cost time = 2.10697
//case 2 [better] cost time = 1.260123
|
分析
Lua使用的是基于寄存器的虚拟机(使用一个栈来提供寄存器),所有的local变量都会保存在寄存器里,因此访问local变量的速度会快于全局变量。在Lua的源码中可以看到对局部变量的最大数量有200的限制:
尾递归调用
对于一些情况下的函数递归调用,转为尾递归写法,可以减少调用栈的分配,提高函数运行效率同时也避免了栈溢出的风险。
测试代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| local n = 0
local add = function() n = n + 1 end
local function (times) if times <= 0 then print("now n = " .. n) return end add() return fr(times - 1) end
local a, b a = os.clock()
fr(100000)
b = os.clock() print(b-a)
|
运行以上代码得到case1和case2的输出分别为:
1 2 3 4 5 6 7
| //case 1 [better] now n = 100000 0.020423
//case 2 now n = 100000 0.050295
|
分析
fr
调用add
后不会再做任何事情,这种情况下当被调用函数add
结束时程序不需要返回到调用者fr
;尾调用不需要使用额外的栈空间,因此尾调用递归的层次可以无限制的。
在Lua的源代码lvm.c
中可以看到以下内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| case OP_CALL: { int b = GETARG_B(i); int nresults = GETARG_C(i) - 1; if (b != 0) L->top = ra+b; /* else previous instruction set top */ L->savedpc = pc; switch (luaD_precall(L, ra, nresults)) { case PCRLUA: { nexeccalls++; goto reentry; /* restart luaV_execute over new Lua function */ } case PCRC: { /* it was a C function (`precall' called it); adjust results */ if (nresults >= 0) L->top = L->ci->top; base = L->base; continue; } default: { return; /* yield */ } } } case OP_TAILCALL: { int b = GETARG_B(i); if (b != 0) L->top = ra+b; /* else previous instruction set top */ L->savedpc = pc; lua_assert(GETARG_C(i) - 1 == LUA_MULTRET); switch (luaD_precall(L, ra, LUA_MULTRET)) { case PCRLUA: { /* tail call: put new frame in place of previous one */ CallInfo *ci = L->ci - 1; /* previous frame */ int aux; StkId func = ci->func; StkId pfunc = (ci+1)->func; /* previous function index */ if (L->openupval) luaF_close(L, ci->base); L->base = ci->base = ci->func + ((ci+1)->base - pfunc); for (aux = 0; pfunc+aux < L->top; aux++) /* move frame down */ setobjs2s(L, func+aux, pfunc+aux); ci->top = L->top = func+aux; /* correct top */ lua_assert(L->top == L->base + clvalue(func)->l.p->maxstacksize); ci->savedpc = L->savedpc; ci->tailcalls++; /* one more call lost */ L->ci goto reentry; } case PCRC: { /* it was a C function (`precall' called it) */ base = L->base; continue; } default: { return; /* yield */ } } }
|
由于没有系统研读过Lua的源码,仅结合注释及上下文做出一些判断。Lua中高端函数调用与函数尾调用使用的是两个指令(OP_CALL
和OP_TAILCALL
),二者很相似,都是通过luaD_precall
执行函数调用,并根据调用了Lua函数还是C函数进行处理。在OP_TAILCALL
中,调用Lua函数之后,可以看到有这样的操作:取到“previous frame”即之前的调用信息CallInfo *ci
,使用新增的CallInfo中的信息取替换ci
中的内容,并移除新增的CallInfo。直接复用CallInfo
从而调用函数不增加栈的深度。
减少rehash
初始化一个表时,可以直接在构造表的时候为表内的key赋值,这样做会好于创建空表再为其填入键和值。因为后者可能触发大量rehash。
测试代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| local function gettable_1() local t = { a=1, b=2, c=3, d=4, e=5, f=6, g=7, h=8, i=9, j=10 } return t end
local function gettable_2() local t = {} t.a=1 t.b=2 t.c=3 t.d=4 t.e=5 t.f=6 t.g=7 t.h=8 t.i=9 t.j=10 return t end
local a,b a = os.clock()
local t={} for i = 1,100000 do
t = gettable_2() end
b=os.clock() print("cost time " .. b - a)
|
运行以上代码得到case1和case2的输出分别为:
1 2 3 4 5
| //case 1 [better] cost time 0.126207
//case 2 cost time 0.332967
|
分析
Lua中表的实现涉及到一些巧妙的算法。每个表都包含两个部分:array部分和hash部分。array部分保存以整数1到n为键的值,其中n是一些特定的值(稍后会说n是怎么算出来的)。所有的其它键值对(包括整数键但是整数超出1到n的范围)保存在hash部分。
如其名所示,hash部分使用hash算法来存储和查找键。它使用的是开放地址法(open address table),所有的键值对都存储在一个hash数组中。使用hash函数来为键取得索引;如果出现hash冲突(即两个不同的键hash得出了相同的索引位置),对应的键会存在一个list列表中,列表中每个元素对应的是一个hash数组中的元素。
当Lua需要向表中插入新的值并且此时hash数组已满时,就会触发rehash。rehash的第一步就是计算新的array部分和hash部分的尺寸。为此,Lua会遍历所有的键值对,计数并分类,然后选择能够使array部分的元素填充率超过一半的最大的2的次方数作为array部分的新尺寸。而hash部分的尺寸是能够容纳所有剩余元素的2的次方数(与array部分的策略不同)。
当创建空表时,array部分和hash部分的尺寸都是0,因此插入新的值的时候就会频繁地触发rehash。
字符串效率
这里基于Lua5.1(5.2开始有长短字符串之分,部分逻辑可能与这里不一样,但结论相似)。对于短小的字符串拼接(或字符串与数字拼接),直接使用..
。对于较大量的字符串的拼接,存入table使用table.concat
来拼接性能更佳。
测试代码
比较string.format
和 拼接..
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| local tostring = tostring local format = string.format
local function stringformat_1(str,num) return str.." " .. num end
local function stringformat_2(str,num) return format("%s %d",str,num) end
local n = 1 local function getargs() return tostring(n),n end
collectgarbage("collect") collectgarbage("stop")
local a,b a = os.clock()
for i = 1,1000000 do
local str = stringformat_2(getargs())
end
b=os.clock() print("cost time " .. b - a)
local c,d c= collectgarbage("count") collectgarbage("collect") d = collectgarbage("count")
print("gc " .. c - d)
|
运行以上代码得到case1和case2的输出分别为:
1 2 3 4 5 6 7
| //case 1 [better] cost time 1.222692 gc 0.2373046875
//case 2 cost time 1.533907 gc 0.9404296875
|
接下来是拼接..
和 table.concat
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| local unpack = unpack or table.unpack
local function stringconcat_1(...) local args = {...} local ret = "" for i,v in ipairs(args) do ret = ret .. v end return ret end
local function stringconcat_2(...) local args = {...} local ret = table.concat(args) return ret end
local t={} for i = 1,10000 do t[i] = tostring(i) end版权声明: 本博客所有文章除特别声明外,均采用 null 许可协议。转载请注明来自 安全书! |