lua优化经验总结

请尊重原作者的工作,转载时请务必注明转载自:www.xionggf.com

这是我在做一些手游项目时的lua优化经验总结。

注意清空table操作的效率问题

在Lua中,通常要清空一个table,很常见的方法就是直接赋值一个新的空table给指向table的变量:例如:

1local t = {I am a table} 
2t = {} - 现在t指向的table就是一个空的新建的table了
3这样子虽然逻辑上没什么问题,但t原来指向的那个table实质上就是一个“野table”了,它的何时被清理掉依赖于lua的gc操作。如果t本身就是存储着一些原生数字类型的数组的话,这种={}的清空操作是比较低效的。如果在每帧每秒都被调用的函数中,清空一个既有的table,可以采用以下的代码:
4-- [[这样子才是全部清空一个table中的所有项,如果将pairs函数改用ipairs函数则只会清空数组项,而不清空hash项]]
5for i, _ in pairs(t) do -- 不需要kv对中的value值,所以用哑元_代替
6    t[i] = nil
7end

利用逻辑判断的短路判定特性优化代码

对于逻辑或(or)操作,只要有一个判断条件为true,则结果为true。逻辑与操作,只要有一个判断条件为false,结果即为false,根据这个特性,在多条件判断时,将获取条件耗费性能最大的函数,挪到最后,尽可能地减少计算。例如: 逻辑与(and)操作,只要有一个判断条件为false,结果即为false。所以对于全逻辑与的操作,在计算判断条件时,应该每计算一次就判断一次,只要有一个为false,就立即得到判定逻辑与计算最终结果为false:

字符串常见问题

和Java,C#,Python等语言类似,一个字符串(对象)对应着一个常量字符串,一旦该字符串声明完毕,其指向的字符串内容即不可更改,所以如果有以下的代码:

1local s1 = I
2local s2 =  am
3local s3 =  Lua
4local s = s1..s2..s3

在第一个连字符操作完成后,会生成一个匿名的常量字符串,尔后该匿名字符串再和s3进行拼接,再生成一个新的字符串赋值给变量s。也就是说,Lua中的所有字符串,都不会发生“in place”操作,一切会对本字符串的内容发生了修改的操作,必然会导致一个新的字符串的产生。如果是存在着较多的字符串连接操作的话,可以使用table来模拟C#的StringBuilder,如下:

 1-- table.concat函数所处理的表中,表项值只接受字符串和数字
 2function concat_string(s1,s2,s3,s4,s5,s6)
 3    -- 如果预先知道了要拼接多少个字符串,在定义table时可以预先
 4    -- 开好多少个空位而不是直接创建空表,这样子有利于性能提升
 5    local t = {nil,nil,nil,nil,nil}
 6    t.name = I am table t  -- table.concat函数无视table中的hash项
 7    t[1] = s1 ; t[2] = s2 ; t[3] = s3
 8    t[4] = s4 ; t[5] = s5 ; t[6] = s6
 9    t[8] = 789 -- 因为t[7]没定义形成空洞,所以字符串拼接只到t[6]为止,无视t[8]
10    return table.concat(t) -- 返回一个字符串
11end
12
13-- 输出为I am a Lua Programmer!
14-- 如果直接用..操作符的话,要产生5个匿名的临时字符串,性能低下!
15print(concat_string("I"," am"," a"," Lua"," Programmer","!"))

Lua函数(闭包)的问题

执行一个函数语句时,会创建一个闭包。通常,对GC的影响不大,但是如果循环中包含函数语句,则可能会产生大量垃圾。例如:

1for i = 1 , 100 do
2    local foo = function(i)
3        print(i)
4    end
5end

上述的代码将会创建100个函数闭包。

尽量用local变量代替global变量

如果需要频密地调用全局的库函数,尽可能地把库函数赋值到一个局部变量中去操作,例如:

1function test_func(t)
2    local local_next = _G.next --在执行密集操作之前,把全局的next函数赋值给局部变量
3    local i, v = local_next (t, nil) --拿到想遍历的table表的首项
4    while i do
5        i, v = local_next (t, i)  --依次遍历表中每项,直到i为nil为止
6    end
7end

这是因为lua虚拟机优先把局部变量存储在虚拟机的寄存器中,其查询速度远远快于查找存储在_G表某个节点下的全局变量。

注意数学计算式的优化

能乘就不要除,能乘法就不要乘方,能平方就不要开方。类似这样的除以一个常量的计算式:

1x * (1/3)

应改为:

1x * 0.33333333333333

乘法操作x*x比乘方操作x^2要快。任何一个语言的开平方操作都是十分耗费性能。 常量计算尽量提早,尽量减少变量计算,例如语句:

11+2+x

就会比x+1+2快。原因就是Lua的语法分析还没那么智能,x+1+2将会拆分成对x变量的两次加法操作,而1+2+x的话,在语法分析时已经能把1+2视为常量计算好了。只会执行一次对x变量的加法操作。

注意断言函数asset的性能

标准库的asset函数用来判断某个测试条件是否满足,如若不满足则发生断言。标准库asset函数的定义如下:

1assert (v [, message])

当参数v的值为非false或者非nil的时候,此函数直接返回v,而如果v为false或者nil的时候,则返回传递进来的message变量,如果不设置message值的话,返回一个字符串“assertion failed!”。标准库的assert函数的问题在于,只要你指定message参数,那么无论assert是否成功与否,都会对message进行求值,例如有以下的代码:

1assert(x <= x_max, "exceeded maximum ("..x_max..")")

也就是说,无论x<=x_max的结果是否为false,第二个参数都会进行求值。显然如果该处代码被频密执行的话,这是一个性能的盆景,因此高效的做法应该是将message参数的求值延迟。可以实现一个更高效的assert函数如下:

 1function efficient_assert(condition, ) -- message参数为可变参数,个数任意
 2    if not condition then -- 如果判断条件为nil或者false则执行对应的操作
 3    --[[调用全局库函数,判断是否有传递message参数,如果没的话,next函数将会
 4        返回一个nil值,表示返回默认字符串“assertion failed!”即可。]]
 5        if next({  }) then
 6            --[[使用标准库pcall函数,对待执行的函数(即pcall的第一个参数),以
 7                保护模式的方式执行调用。pcall的第二个可变参数就是传递给第一个参
 8                数的函数形参,之所以使用pcall函数,原因就是在于string.format
 9                函数在执行时也有可能产生异常,使用pcall是为了内部处理掉这个可能
10                抛出的异常,将焦点集中在要判断的]]
11            local s , r = pcall( function () 
12                                    return string.format())
13                                    end ,  )
14
15            --[[如果pcall调用正常了,变量s就指向string.format拼接好的字符串,
16                然后调用error函数,中止刚才pcall执行的函数,并且返回变量s,
17                error函数的第二个参数传递了2,指明错误不是本函数内部产生的,
18                而是因为调用者的错误导致]]
19            if s then
20                error(assertion failed!: ..r,2)
21            end
22
23            error(assertion failed!”,2)
24        end
25    end
26end