lua优化经验总结
Table of Contents
请尊重原作者的工作,转载时请务必注明转载自:www.xionggf.com
这是我在做一些手游项目时的lua优化经验总结。
注意清空table操作的效率问题
在Lua中,通常要清空一个table,很常见的方法就是直接赋值一个新的空table给指向table的变量:例如:
local t = {“I am a table”}
t = {} –- 现在t指向的table就是一个空的新建的table了。
这样子虽然逻辑上没什么问题,但t原来指向的那个table实质上就是一个“野table”了,它的何时被清理掉依赖于lua的gc操作。如果t本身就是存储着一些原生数字类型的数组的话,这种={}的清空操作是比较低效的。如果在每帧每秒都被调用的函数中,清空一个既有的table,可以采用以下的代码:
-- [[这样子才是全部清空一个table中的所有项,如果将pairs函数改用ipairs函数则只会清空数组项,而不清空hash项]]
for i, _ in pairs(t) do -- 不需要kv对中的value值,所以用哑元_代替
t[i] = nil
end
利用逻辑判断的短路判定特性优化代码
对于逻辑或(or)操作,只要有一个判断条件为true,则结果为true。逻辑与操作,只要有一个判断条件为false,结果即为false,根据这个特性,在多条件判断时,将获取条件耗费性能最大的函数,挪到最后,尽可能地减少计算。例如: 逻辑与(and)操作,只要有一个判断条件为false,结果即为false。所以对于全逻辑与的操作,在计算判断条件时,应该每计算一次就判断一次,只要有一个为false,就立即得到判定逻辑与计算最终结果为false:
字符串常见问题
和Java,C#,Python等语言类似,一个字符串(对象)对应着一个常量字符串,一旦该字符串声明完毕,其指向的字符串内容即不可更改,所以如果有以下的代码:
local s1 = “I”
local s2 = “ am”
local s3 = “ Lua”
local s = s1..s2..s3
在第一个连字符操作完成后,会生成一个匿名的常量字符串,尔后该匿名字符串再和s3进行拼接,再生成一个新的字符串赋值给变量s。也就是说,Lua中的所有字符串,都不会发生“in place”操作,一切会对本字符串的内容发生了修改的操作,必然会导致一个新的字符串的产生。如果是存在着较多的字符串连接操作的话,可以使用table来模拟C#的StringBuilder,如下:
-- table.concat函数所处理的表中,表项值只接受字符串和数字
function concat_string(s1,s2,s3,s4,s5,s6)
-- 如果预先知道了要拼接多少个字符串,在定义table时可以预先
-- 开好多少个空位而不是直接创建空表,这样子有利于性能提升
local t = {nil,nil,nil,nil,nil}
t.name = “I am table t” -- table.concat函数无视table中的hash项
t[1] = s1 ; t[2] = s2 ; t[3] = s3
t[4] = s4 ; t[5] = s5 ; t[6] = s6
t[8] = 789 -- 因为t[7]没定义形成空洞,所以字符串拼接只到t[6]为止,无视t[8]
return table.concat(t) -- 返回一个字符串
end
-- 输出为I am a Lua Programmer!
-- 如果直接用..操作符的话,要产生5个匿名的临时字符串,性能低下!
print(concat_string("I"," am"," a"," Lua"," Programmer","!"))
Lua函数(闭包)的问题
执行一个函数语句时,会创建一个闭包。通常,对GC的影响不大,但是如果循环中包含函数语句,则可能会产生大量垃圾。例如:
for i = 1 , 100 do
local foo = function(i)
print(i)
end
end
上述的代码将会创建100个函数闭包。
尽量用local变量代替global变量
如果需要频密地调用全局的库函数,尽可能地把库函数赋值到一个局部变量中去操作,例如:
function test_func(t)
local local_next = _G.next --在执行密集操作之前,把全局的next函数赋值给局部变量
local i, v = local_next (t, nil) --拿到想遍历的table表的首项
while i do
i, v = local_next (t, i) --依次遍历表中每项,直到i为nil为止
end
end
这是因为lua虚拟机优先把局部变量存储在虚拟机的寄存器中,其查询速度远远快于查找存储在_G表某个节点下的全局变量。
注意数学计算式的优化
能乘就不要除,能乘法就不要乘方,能平方就不要开方。类似这样的除以一个常量的计算式:
x * (1/3)
应改为:
x * 0.33333333333333
乘法操作x*x比乘方操作x^2要快。任何一个语言的开平方操作都是十分耗费性能。 常量计算尽量提早,尽量减少变量计算,例如语句:
1+2+x
就会比x+1+2快。原因就是Lua的语法分析还没那么智能,x+1+2将会拆分成对x变量的两次加法操作,而1+2+x的话,在语法分析时已经能把1+2视为常量计算好了。只会执行一次对x变量的加法操作。
注意断言函数asset的性能
标准库的asset
函数用来判断某个测试条件是否满足,如若不满足则发生断言。标准库asset
函数的定义如下:
assert (v [, message])
当参数v的值为非false或者非nil的时候,此函数直接返回v,而如果v为false或者nil的时候,则返回传递进来的message变量,如果不设置message值的话,返回一个字符串“assertion failed!”。标准库的assert函数的问题在于,只要你指定message参数,那么无论assert是否成功与否,都会对message进行求值,例如有以下的代码:
assert(x <= x_max, "exceeded maximum ("..x_max..")")
也就是说,无论x<=x_max的结果是否为false,第二个参数都会进行求值。显然如果该处代码被频密执行的话,这是一个性能的盆景,因此高效的做法应该是将message参数的求值延迟。可以实现一个更高效的assert函数如下:
function efficient_assert(condition, …) -- message参数为可变参数,个数任意
if not condition then -- 如果判断条件为nil或者false则执行对应的操作
--[[调用全局库函数,判断是否有传递message参数,如果没的话,next函数将会
返回一个nil值,表示返回默认字符串“assertion failed!”即可。]]
if next({ … }) then
--[[使用标准库pcall函数,对待执行的函数(即pcall的第一个参数),以
保护模式的方式执行调用。pcall的第二个可变参数就是传递给第一个参
数的函数形参,之所以使用pcall函数,原因就是在于string.format
函数在执行时也有可能产生异常,使用pcall是为了内部处理掉这个可能
抛出的异常,将焦点集中在要判断的]]
local s , r = pcall( function (…)
return string.format(…))
end , … )
--[[如果pcall调用正常了,变量s就指向string.format拼接好的字符串,
然后调用error函数,中止刚才pcall执行的函数,并且返回变量s,
error函数的第二个参数传递了2,指明错误不是本函数内部产生的,
而是因为调用者的错误导致]]
if s then
error(“assertion failed!: ”..r,2)
end
error(“assertion failed!”,2)
end
end
end