几种Lua优化技巧
Lua优化技巧选讲
内存优化
众所周知,Lua使用的是糟糕程度仅次于“手动分配堆内存、指针乱飘”的内存模型——GC模型。这意味着,对于Table、String等复杂类型,只要你搞丢了相应的引用名,对应的东西就会彻底变成垃圾一团,并且不会自动释放,也无法手动要求释放,只能等着垃圾回收器去回收了。
举一个简单的栗子AwA
a = {x = 100,
y = 200}
a = {x = 300,
y = 400}
这里实际上我们构造了两个Table,然后第一个Table的引用(一开始是a)就这么被我们搞丢了,然后第一个Table就变成垃圾了。
一般情况下随意点也没啥大的问题,但我们在Defold做的游戏一般要跑60fps,一些音游甚至跑120fps,考虑这样一个情境:
function update(self,dt)
for k=1,60 do
a = {
x = k-1,
y = k+1
}
b = {
x = k+10,
y = k-10
}
self.my_x += a.x*b.y
self.my_y += a.y*b.x
end
end
一秒跑完那60帧,就制造了7200个表的垃圾,这下非常可观了。
因此,多复用现有的表:
local a = {
x = 0,
y = 0
}
local b = {
x = 0,
y = 0
}
function update(self,dt)
for k=1,60 do
a.x = k-1
a.y = k+1
b.x = k+10
b.y = k-10
end
self.my_x += a.x*b.y
self.my_y += a.y*b.x
end
这下update一个表都不会造了www
另外,大家提得比较多的就是String不可变的问题,先给个不好的例子:
names = {"甲","乙","丙","丁","戊"}
ages = {19,21,20,19,18}
position = "中南大学"
for i,value in ipairs(names) do
print(value .. "," .. ages[i] .. "岁,现就职于" .. position)
end
-- 甲,19岁,现就职于中南大学
-- 乙,21岁,现就职于中南大学
-- 丙,20岁,现就职于中南大学
-- 丁,19岁,现就职于中南大学
-- 戊,18岁,现就职于中南大学
为了打出这5句狗话,系统额外申请了30块堆区内存用来存放拼接过程。。。。。。
然后这里有个好点的例子:
names = {"甲","乙","丙","丁","戊"}
ages = {19,21,20,19,18}
position = "中南大学"
pre_str = "%s,%d岁,现就职于%s"
for i=1,#names do
local concat = string.format(pre_str, names[i], ages[i], position)
print(concat)
end
现在我们引入print( collectgarbage("count") )
统计下内存消耗:

差距还是挺大的,嗯。
最后,Lua非常友善地提供了让GC间歇运行的方法,考虑到游戏引擎往往提供更新函数,洁癖比较深重的人推荐试试:
function init(self)
collectgarbage("stop") --关闭GC触发
-- 然后干点别的
end
function update(self,dt)
collectgarbage("step",4) --每帧回收相当于4KB内存的垃圾
-- 这里干点别的
end
最后的最后……
Lua如果像上面一样单步跑GC的话,可以借助协程把GC任务剥离到单独的线程,但这样做意义不大;如果是正常的倍率步进GC的话,让GC单独占一个线程需要魔改Lua的本体——有点超过我们的讨论范围了,抱歉。
运行效率优化
首先是缓存这件事,门道很深,先从Local部分讲起。
有关Lua的一切,都是围绕着“Table”这样一种数据结构展开的。举例来说,为了允许把函数存储在表内,Lua做了First-Class函数,甚至闭包之类的支持。
我们直接在命令行打开Lua走交互式编程,写点这种东西:
a = 2
b = 6
c = a*110 + b
print(c) --226
那么……这里的a、b、c在什么地方呢?答曰:储存在全局表_G里。
Lua的Table是一种相当之兼容并蓄的数据结构,而最常见的(非嵌套)Table大概长这两种样子:
hero = {
hp = 100,
mp = 100,
weapon = "sword"
}
main_subjects = {"Chinese","Math","English"}
前者对应了一些语言的哈希表、字典,后者对应一些动态语言的Array容器。
然后我们尝试从这两种Table里面拿点东西:
-- 书接上文
print(hero.hp)
-- 100
print(main_subjects[1]) -- "Chinese"
取hero表里的hp字段时,Lua首先会把"hp"这么一串字符作哈希化处理,然后在描述“hero”这个表的Hash Map里逐个问询其中的元素,看看指向哪块内存。
于是,执行任何东西都无法避免这么一个查表轮询的过程,相当低效。因此,Lua的虚拟机提供了一种类似于CPU“寄存器”的东西,并且把它用local
这么一个关键字暴露了出来,顺便为其增加了作用域相关的规则。
这也是为什么Lua的局部变量全都要显式声明。
就Defold引擎的Lua脚本而言,更新、输入、消息函数调用频度很高,对这些地方做Local缓存优化效果会很明显。
这里介绍一下个人比较倾向的优化方式,先捡一段未经优化的脚本出来:
function on_message(self, message_id, message, sender)
if message_id == hash("collision_response") then
msg.post("main#gui", "add_score", {amount = score})
particlefx.play("#pickup")
go.delete()
end
end
接着先做一下Local优化,以及Table复用优化:
local haxi = hash
local msg_post = msg.post
local particlefx_play = particlefx.play
local go_delete = go.delete
local scr_table = {amount = score}
function on_message(self, message_id, message, sender)
if message_id == haxi("collision_response") then
scr_table["amount"] = score
msg_post("main#gui", "add_score", score)
particlefx_play("#pickup")
go_delete()
end
end
可以看到,基本上都是体力活。。。。。。
就Defold而言,hash值、定位地址也是可以缓存的,这里进一步对hash值作一次缓存:
local collision_response = hash("collision_response")
local msg_post = msg.post
local particlefx_play = particlefx.play
local go_delete = go.delete
local scr_table = {amount = score}
function on_message(self, message_id, message, sender)
if message_id == collision_response then
scr_table["amount"] = score
msg_post("main#gui", "add_score", score)
particlefx_play("#pickup")
go_delete()
end
end
String其实也可以预存一下,但是收益不大——因为Lua在识别到重复的String时,会自动转换为对原有String的引用,并不存在同一String多次开辟内存的行为。因此,预存String可以稍微提升一点点性能。
但就Defold采用的LuaJIT而言,Local变量过多可能会导致JIT放弃编译,会有些得不偿失,因此String预存的优先级应该靠后一些。
然后让我们看向main_subjects[1]
部分。这个语句恰好就是main_subject这个表的第一个元素,针对这种情况应该就没有必要再开个Hash Map然后大找特找了。
Lua在设计时也考虑到了这一点。因此,Table在内部其实区分成了Array Part和Hash Part,并且Array Part不需要查表的这个步骤、和数组更像一点。
因此,多使用Array Part;然后,尊重一下Array的有序性,尽量预先定好Array的大小不要轻易改变。
此外,LuaJIT提供了Array预分配大小的支持:
local table_new = require "table.new"
local my_new_array = table_new(100,0)
-- 新建一个Array部分预分配100个元素、Hash部分不预分配元素的Table
-- my_new_array是刚刚新建的Table的引用
但是Defold Script不能引入这个函数,洗洗睡吧()
接下来让我们谈谈遍历。在LuaJIT的世界里,for i=1,#table
和for k,v in ipairs(table)
效果差不多,都远远优于for k,v in pairs(table)
。原因无他,LuaJIT对pairs()
的JIT支持还是Not Yet Implemented。。。。。。
最后,不推荐硬写OOP。Lua对OOP优化远低于正经OOP语言,建议选取更扁平的数据结构。
同样给个糟糕的例子:
--创建一个类,表示四边形
local RectAngle = {length, width, area}
--声明类名和类成员变量
function RectAngle: new (len,wid)
--声明新建实例的New方法
local o = {
--设定各个项的值
length = len or 0,
width = wid or 0,
area =len*wid
}
setmetatable(o,{__index = self} ) --将自身的表映射到新new出来的表中 return o
end
然后来个比较简单、优化的例子,假设我们最后用到100个矩形:
local table_new = require "table.new"
local rect_length = table_new(100,0)
local rect_width = table_new(100,0)
local rect_area = table_new(100,0)
local rect_num = 0
function rect_new(length,width)
rect_num += 1
rect_length[rect_num] = length or 0
rect_width[rect_num] = width or 0
rect_area[rect_num] = length*width or 0
return rect_num
end
function rect_get(id)
return rect_length[id],rect_width[id],rect_area[id]
end
溜了溜了(