欢迎光临散文网 会员登陆 & 注册

几种Lua优化技巧

2022-10-01 11:43 作者:Fuxfantx  | 我要投稿

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") )统计下内存消耗:

截屏2022-09-19 12.57.12.png

差距还是挺大的,嗯。

最后,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,#tablefor 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


溜了溜了(

几种Lua优化技巧的评论 (共 条)

分享到微博请遵守国家法律