lua 性能优化

Posted wang-jin-fu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了lua 性能优化相关的知识,希望对你有一定的参考价值。

飞书文档:https://idreamsky.feishu.cn/docs/doccnjZ7tfpP5AFnSWGnlaUDm1h

一、需要注意的数据类型

1. table

Lua 实现表的算法颇为巧妙。每个表包含两部分:数组(array)部分和哈希(hash)部分,数组部分保存的项(entry)以整数为键(key),从 1 到某个特定的 n,所有其他的项(包括整数键超出范围的)则保存在哈希部分。

哈希部分使用哈希算法来保存和查找键值。它使用的是开放寻址(open address)的表,意味着所有的项都直接存在哈希数组里。键值的主索引由哈希函数给出;如果发生冲突(两个键值哈希到相同的位置),这些键值就串成一个链表,链表的每个元素占用数组的一项。

当 Lua 想在表中插入一个新的键值而哈希数组已满时,Lua 会做一次重新哈希(rehash)。重新哈希的第一步是决定新的数组部分和哈希部分的大小。所以 Lua 遍历所有的项,并加以计数和分类,然后取一个使数组部分用量过半的最大的 2 的指数值,作为数组部分的大小。而哈希部分的大小则是一个容得下剩余项(即那些不适合放在数组部分的项)的最小的 2 的指数值。重新hash的性能消耗还是比较大的。要减少重新hash次数,可以创建大的表格替代多个小的表格或者复用表格。

 

每次新建一张table,都会产生堆内存,都会导致GC遍历的时候多一个判断节点。因此,Lua的GC优化,重点关注table和c的userdata。

    在频繁更新或者使用的代码部分,不要反复申请table,这会使得虚拟机不断的去进行内存分配。

 

1. 追加一个元素到一个array的结尾的三种写法。其中使用本地计数器的第三种写法性能最好。

2. 1.

3. t[#t + 1] =  123

  1. 4.  

5. 2.

6. talbe.insert(t,  123)

  1. 7.  

8. 3.

9. local counter = 1

10. for i = 1, 10000 od

11. t[counter] = i

12. counter = counter + 1

13. end

  1. 14.  

 

配置表优化:见下方。

服务端数据:例如背包中的每个道具数据可以在本地保存一个table,服务端更新时只需要更新对对应道具的table数据而不用每次创建新的表。

其它临时数据:减少在定时器(帧、秒)或update方法里开辟新的空间(引用全局变量、obj.transform等.操作、创建新的表),可以在循环开始前定义一个local变量做缓存。

2. 字符串string

因为Lua的String是内部复用的,当我们创建字符串的时候,Lua首先会检查内部是否已经有相同的字符串了,如果有直接返回一个引用,如果没有才创建。这使得Lua中String的比较和赋值非常地快速,因为只要比较引用是否相等、或者直接赋值引用就可以了。

 

连接方式:多个字符串连接时使用table.concat代替..的字符串连接。table.concat只会创建一块buffer,然后在此拼接所有的字符串,实际上是在用table模拟buffer。而..则每次拼接都会产生一串新的字符串,开辟一块新的buffer。聊天需要更注重这块内容。

3. 结构体如Vector3

为什么结构体单独说呢,因为结构体会带来很严重的性能问题,具体原因可以参考:https://www.jianshu.com/p/07dc38e85923以及https://www.gameres.com/700911.html

简而言之,就是是boxing(装箱)和unboxing(拆箱)。Vector3(栈)转为object类型需要boxing(堆内存中),object转回Vector3需要unboxing,使用后释放该object引用,这个堆内存被gc检测到已经没引用,释放该堆内存,产生一个gc内存。

 

toluaslua 将Vector3等类型实现为纯lua代码,Vector3就是一个{x,y,z}的table,这样在lua中使用就快了。因为以上结构体,都是table的方式,所以,如果使用频繁的话,就容易产生大量的堆内存,必要的时候还是用对象池复用,例如坐标系统的点坐标。

使用c#原生的vector,建议在c#端进行封装,传值时使用x,y,z进行传递,在c#层包装成vector使用。直接在函数中传递三个float,要比传递Vector3要更快。

例如void SetPos(GameObject obj, Vector3pos)改为void SetPos(GameObject obj, float x, floaty, float z)

二、lua测优化

参考:https://www.lua.org/gems/sample.pdf

1.首先,我们需要了解类的实现,如下,核心的代码是setmetatable(cls, {__index = super})这句,访问 cls 中任何不存在的字段时,都会尝试到 super 中查找,这里的 super 就相当于父类,而 cls 则相当于是类 super 的子类。

1. local function __class(classname, super)

  1. 2.     local superType = type(super)
  2. 3.     local cls
  3. 4.  
  4. 5.     if superType ~= "function" and superType ~= "table" then
  5. 6.         superType = nil
  6. 7.         super = nil
  7. 8.     end
  8. 9.  
  9. 10.     if superType == "function" or (super and super.__ctype == 1) then
  10. 11.         -- inherited from native C++ Object
  11. 12.     else
  12. 13.         -- inherited from Lua Object
  13. 14.         if super then
  14. 15.             cls = {}
  15. 16.             setmetatable(cls, {__index = super})
  16. 17.             cls.super = super
  17. 18.         else
  18. 19.             cls = {ctor = function() end}
  19. 20.         end
  20. 21.  
  21. 22.         cls.__cname = classname
  22. 23.         cls.__ctype = 2 -- lua
  23. 24.         cls.__index = cls
  24. 25.  
  25. 26.         function cls.ToString(self)
  26. 27.             return self.__cname
  27. 28.         end
  28. 29.  
  29. 30.         function cls.new(...)
  30. 31.             local instance = setmetatable({}, cls)
  31. 32.             instance.class = cls
  32. 33.             instance:ctor(...)
  33. 34.             return instance
  34. 35.         end
  35. 36.     end
  36. 37.  
  37. 38.     return cls

39. end

元表:当访问表中不存在的字段时,元表中的 __index 元方法会被调用,并返回该方法返回的值,该值可以是一个函数或者表。注意,这边是访问不了元表内的属性的,而是去获取__index属性的返回值,如果返回值是函数则调用,返回表则在表内查找字段。测试如下,man无法访问Person类的isMan字段,但可以访问__index内的字段name。这就解释了继承为什么是setmetatable(cls, {__index = super}),而不是setmetatable(cls, super)。

1. local Person = {

  1. 2.     isMan = true,
  2. 3.     __index = {
  3. 4.         name = "jadeshu",
  4. 5.         age = 28,
  5. 6.         sex = 0,
  6. 7.     }

8. }  --表

  1. 9.  

10. local man = {}  --表

11. setmetatable(man,Person)  --设置元表

12. --man的元表是Person

  1. 13.  

14. --测试

15. printWJF(man.name)  --显示 jadeshu

16. printWJF(man.isMan,"_",Person.isMan) --显示 nil_true

2.local变量和_G全局变量、self变量

_G:一张表,保存了lua所用的所有全局函数和全局变量,在默认情况,Lua在全局环境_G中添加了标准库比如math、函数比如pairs等。如_G.print("你好")=print("你好")。

全局变量不需要声明,没被 local 修饰的变量都是全局变量。我们应该减少全局变量的定义,可以把一些全局的属性放在一个全局表里,在通过这个表访问。

local:局部变量只在被声明的那个代码块内有效。(代码块:指的是一个控制结构内,一个函数体,或者一个chunk(变量被声明的那个文件或者文本串)),无法通过继承、元表访问,类似于c#的private变量。

需要注意的是:使用function声明的函数为全局函数,在被引用时不会因为声明的顺序而找不到 ,使用local function声明的函数为局部函数,在引用的时候必须要在声明的函数后面。

local_g的优劣见:http://lua-users.org/wiki/OptimisingUsingLocalVariables

1. Local variables are very fast as they reside in virtual machine registers, and are accessed directly by index. Global variables on the other hand, reside in a lua table and as such are accessed by a hash lookup.

所以尽量使用local变量。local变量包括属性以及方法,一些经常或在循环用到的全局函数,可以申明为local局部变量,这样可以提升效率。例如表插入操作local TINSERT = table.insert。

 

self:代表当前表(模块),可以理解成c#的this,子类可以访问父类的self属性,不能访问local属性。如下,module作为父类或被require加载出来后,lParam 不能在模块外部访问,他们并不在最后return的module表里。constant和constant1做为module表里的内容可以被外部访问。

self.xxx定义的变量访问速度比local较慢,因为self查找会走元表,如果多重嵌套,效率肯定是比不上local的,但是self变量可以被外部模块访问,一些需要提供给外部的数据比较方便,当然你也可以把local封装一个Get方法。

1. -- 文件名为 module.lua

2. -- 定义一个名为 module 的模块

3. module = {}

  1. 4.  

5. local lParam = "这是一个局部变量"

  1. 6.  

7. -- 定义一个常量

8. module.constant = "这是一个公共变量"

  1. 9.  

10. -- 定义一个函数

11. function module:func1()

  1. 12.     self.constant1 = "这也是一个公共变量"

13. end

  1. 14.  

15. local function func2()

  1. 16.     print("这是一个私有函数!")

17. end

18. return module

3.配置表

缓存:使用时动态加载。

缓存处理一般有:1.常驻内存,加载后不销毁;2.定时清理,加载后一定时间内未使用则清理,使用则刷新时间;3.一次性,不缓存;4.跟随场景,只在切换场景时清除配置表;

优化:参考https://blog.uwa4d.com/archives/1490.html。核心点是

1.通过工具将excel表转为lua文件,通过table的方式访问表格。

2.提取配置表中大量重复的默认值、表格、数组等作为表的元表,减少重复变量尤其是重复的空表。

3.对配置表中只在客户端、服务端单项使用的字段进行分离,也就是说只有服务端用到的字段不导出到客户端的表格。

4.字符串处理,例如说明字段、标题等配置在多语言的表格里,在使用key值索引到对应的多语言项,多语言配置最好一个语言一张表,当前游戏使用哪个语种就加载哪个配置文件。

最终结构类似于:ARENA下的每一条数据的元表设置成默认值,当在数据里找不到指定key,会在元表(也就是默认值defaultValues)里查找默认值。这边的设置_index实际上相当于设置父类,当前表里查不到对象时,会在_index对象内查找,具体可以看源码里lua class的实现。

1. local defaultValues = {

  1. 2.    robotName = "des_3115",

3. }

  1. 4.  

5. local ARENA = {

  1. 6.    [1] = { rank = { 1, 1, }, robotGroupId = 5000, },
  2. 7.    [2] = { rank = { 2, 2, }, robotGroupId = 4999, },
  3. 8.    [3] = { rank = { 3, 3, }, robotGroupId = 4998, },
  4. 9.    [4] = { rank = { 4, 4, }, robotGroupId = 4997, },
  5. 10.    [5] = { rank = { 5, 5, }, robotGroupId = 4996, },
  6. 11.    [6] = { rank = { 6, 6, }, robotGroupId = 4995, },
  7. 12.    [7] = { rank = { 7, 7, }, robotGroupId = 4994, },

13. }

  1. 14.  

15. do

  1. 16.     local base = {
  2. 17.         __index = defaultValues, --基类,默认值存取
  3. 18.         __newindex = function()
  4. 19.             --禁止写入新的键值
  5. 20.             error("Attempt to modify read-only table")
  6. 21.         end
  7. 22.     }
  8. 23.     for k, v in pairs(ARENA) do
  9. 24.         setmetatable(v, base)
  10. 25.     end
  11. 26.     base.__metatable = false --不让外面获取到元表,防止被无意修改

27. end

  1. 28.  

29. return ARENA

4.不要在for循环中创建表和闭包

1. local t = {1,2,3,‘hi‘}

2. for i=1,n do

  1. 3.     --执行逻辑,但t不更改
  2. 4.     ...

5. end

5.建议在场景切换时主动调用一次GC,包括luac#gc方法。

三、与c#的交互优化

1. 交互优化

参考:https://gameinstitute.qq.com/community/detail/125117

gameobj.transform.position = pos调用栈如下:

 

调用函数:Lua中如果要调用一次C#的函数,至少有几个步骤:

1、在Lua层面,找到C#这个函数的Wrapper的C指针

2、C#层面,进行参数个数,参数类型的验证

3、不同类型的参数校验成本又是不一样的

Number类型,调用LuaDLL.luaL_checknumber  进行一次验证即可

String类型,需要先LuaDLL.lua_type 获取类型,根据不同类型再调用一次LuaDLL的对应tostring接口

Struct类型,如Vector3等,需要调用LuaDLL.tolua_getvec3获取结构体的值,再new一个Vector3

   4、返回值处理

 

优化建议:

尽量减少不需要的交互,能在lua完成的就在lua完成。

lua端减少长串的点号操作,例如child.parent.tranfrom.localposition,建议在c#封装SetParentLocalPosition方法。

lua端减少对结构体(如vector)的直接操作,频繁使用的可以用tolua等封装的组件,非频繁的可以在c#额外封装方法,示例如下。

1. public static DateTime GetLocalServerTime() {

  1. 2.    return com.geargames.common.utils.Utils.ServerDateTimeNow().GGToLocalTime();

3. }

4. public static void SetLocalPositionEx(this Component cmpt, float x, float y, float z)

5. {

  1. 6.     cmpt.transform.localPosition = new Vector3(x, y, z);

7. }

  1. 8.  

运行效率测试脚本如下,访问次数为100000次:

例1:

1、local pos = me.Root.transform.position

2、local pos = me.Root:GetLocalPosition()

3、local x,y,z = me.Root:GetLocalPositionEx()

测试结果(单位秒):

0.22617602348328

0.1167140007019

0.052457094192505

例2:

local y = me.Root.transform.localPosition.y

local y = me.Root:GetLocalPositionY()

测试结果:

0.2229311466217

0.052457094192505

测试代码:

1. public static float GetLocalPositionY(this Component cmpt) {

  1. 2.     return cmpt.transform.localPosition.y;

3. }

4. public static void GetLocalPositionEx(this Component cmpt, out float x, out float y, out float z)

5. {

  1. 6.      Transform trans = cmpt.transform;
  2. 7.      x = trans.localPosition.x;
  3. 8.      y = trans.localPosition.y;
  4. 9.      z = trans.localPosition.z;
  5. 10.   }
  6. 11.  

12. local pos = Vector3(0,0,0)   

13. for i=1,100000 do       

  1. 14.     me.Root:SetLocalPosition(pos) --0.058831930160522 
  2. 15.     --me.Root:SetLocalPositionEx(0,0,0)    -- 0.063822984695435
  3. 16.     --me.Root:SetLocalPosition(Vector3(0,0,0))  --0.13435101509094

17. End

lua端减少频繁获取unity组件(如GetComponentFind等方法),频繁使用的组件建议缓存到本地。建议使用导出工具获取需要操作的对象,而不是find的方法,避免忘记释放导致的泄露问题。

减少在循环里通过.获取c#对象的属性,如果可以,请缓存它们。

 

封装方法注意点:

1. luac#之间传参、返回时,尽可能不要传递以下类型:

2. 严重类: Vector3/Quaternion等unity值类型,数组

3. 次严重类:bool string 各种object

4. 建议传递:int float double

5. 频繁调用的函数,参数的数量要控制,无论是lua的pushint/checkint,还是c到c#的参数传递,参数转换都是最主要的消耗,而且是逐个参数进行的,因此,lua调用c#的性能,除了跟参数类型相关外,也跟参数个数有很大关系。

6. 优先使用static函数导出,减少使用成员方法导出

7. 合理利用out关键字返回复杂的返回值

2. 精简lua导出

      网上已经有非常多IL2CPP导致包体积激增的抱怨,而基于lua静态导出后,由于生成了大量的导出代码。这个问题又更加严重。

      鉴于目前ios必须使用IL2CPP发布64bit版本,所以这个问题必须要重视,否则不但你的包体积会激增,binary是要加载到内存的,你的内存也会因为大量可能用不上的lua导出而变得吃紧。

      移除你不必要的导出,尤其是unityengine的导出。如果只是为了导出整个类的一两个函数或者字段,重新写一个util类来导出这些函数,而不是整个类进行导出。也可以使用[notolua]属性来标记不导出。例如我们只用到Animation的Play方法,不需要整个导出Animation类,只需要导出对应方法,或封装一个方法导出。

      如果有把握,可以修改自动导出的实现,自动或者手动过滤掉不必要导出的东西。

3. 引用移除

两端保存的引用及时清除,例如lua持有的c#数据结构。

c# object返回给lua,是通过dictionary将lua的userdata和c# object关联起来,只要lua中的userdata没回收,c# object也就会被这个dictionary拿着引用,导致无法回收。

最常见的就是gameobject和component,如果lua里头引用了他们,即使你进行了Destroy,也会发现他们还残留在mono堆里。

不过,因为这个dictionary是lua跟c#的唯一关联,所以要发现这个问题也并不难,遍历一下这个dictionary就很容易发现。ulua下这个dictionary在ObjectTranslator类、slua则在ObjectCache类。

四、内存优化工具

Lua 提供了以下函数collectgarbage ([opt [, arg]])用来控制自动内存管理:

  • collectgarbage("collect"): 做一次完整的垃圾收集循环。通过参数 opt 它提供了一组不同的功能:
  • collectgarbage("count"): 以 K 字节数为单位返回 Lua 使用的总内存数。 这个值有小数部分,所以只需要乘上 1024 就能得到 Lua 使用的准确字节数(除非溢出)。
  • collectgarbage("restart"): 重启垃圾收集器的自动运行。
  • collectgarbage("setpause"): 将 arg 设为收集器的 间歇率。 返回 间歇率 的前一个值。
  • collectgarbage("setstepmul"): 返回 步进倍率 的前一个值。
  • collectgarbage("step"): 单步运行垃圾收集器。 步长"大小"由 arg 控制。 传入 0 时,收集器步进(不可分割的)一步。 传入非 0 值, 收集器收集相当于 Lua 分配这些多(K 字节)内存的工作。 如果收集器结束一个循环将返回 true 。
  • collectgarbage("stop"): 停止垃圾收集器的运行。 在调用重启前,收集器只会因显式的调用运行。

 

如何监测Lua的编程产生内存泄露:

1. 针对会产生泄露的函数,先调用collectgarbage("collect")和collectgarbage("count"),取得最初的内存使用情况。

2. 函数调用后, collectgarbage("collect")进行收集, 并使用collectgarbage("count")再取得当前内存, 最后记录两次的使用差。

 

可以保存函数调用前后的_G到本地文件,然后使用软件比较前后两次的_G的内容差,可以获取到泄漏的具体内容。文件差异对比软件:https://blog.csdn.net/liuyukuan/article/details/5980591

 

当然,推荐使用现成的工具lua profile,下载及文档链接:https://github.com/ElPsyCongree/LuaProfiler-For-Unity#zh

以上是关于lua 性能优化的主要内容,如果未能解决你的问题,请参考以下文章

Unity+Lua实测如何性能优化(Lua和C#交互篇)

lua 性能优化

lua 性能优化知识点

使用Lua 局部变量来优化性能,同一时候比較局部变量和全局变量

Unreal引擎中Lua代码的性能瓶颈定位与优化

Unity+Lua游戏开发的性能检测!