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
- 4.
5. 2.
6. talbe.insert(t, 123)
- 7.
8. 3.
9. local counter = 1
10. for i = 1, 10000 od
11. t[counter] = i
12. counter = counter + 1
13. end
- 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)
- 2. local superType = type(super)
- 3. local cls
- 4.
- 5. if superType ~= "function" and superType ~= "table" then
- 6. superType = nil
- 7. super = nil
- 8. end
- 9.
- 10. if superType == "function" or (super and super.__ctype == 1) then
- 11. -- inherited from native C++ Object
- 12. else
- 13. -- inherited from Lua Object
- 14. if super then
- 15. cls = {}
- 16. setmetatable(cls, {__index = super})
- 17. cls.super = super
- 18. else
- 19. cls = {ctor = function() end}
- 20. end
- 21.
- 22. cls.__cname = classname
- 23. cls.__ctype = 2 -- lua
- 24. cls.__index = cls
- 25.
- 26. function cls.ToString(self)
- 27. return self.__cname
- 28. end
- 29.
- 30. function cls.new(...)
- 31. local instance = setmetatable({}, cls)
- 32. instance.class = cls
- 33. instance:ctor(...)
- 34. return instance
- 35. end
- 36. end
- 37.
- 38. return cls
39. end
元表:当访问表中不存在的字段时,元表中的 __index 元方法会被调用,并返回该方法返回的值,该值可以是一个函数或者表。注意,这边是访问不了元表内的属性的,而是去获取__index属性的返回值,如果返回值是函数则调用,返回表则在表内查找字段。测试如下,man无法访问Person类的isMan字段,但可以访问__index内的字段name。这就解释了继承为什么是setmetatable(cls, {__index = super}),而不是setmetatable(cls, super)。
1. local Person = {
- 2. isMan = true,
- 3. __index = {
- 4. name = "jadeshu",
- 5. age = 28,
- 6. sex = 0,
- 7. }
8. } --表
- 9.
10. local man = {} --表
11. setmetatable(man,Person) --设置元表
12. --man的元表是Person
- 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 = {}
- 4.
5. local lParam = "这是一个局部变量"
- 6.
7. -- 定义一个常量
8. module.constant = "这是一个公共变量"
- 9.
10. -- 定义一个函数
11. function module:func1()
- 12. self.constant1 = "这也是一个公共变量"
13. end
- 14.
15. local function func2()
- 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 = {
- 2. robotName = "des_3115",
3. }
- 4.
5. local ARENA = {
- 6. [1] = { rank = { 1, 1, }, robotGroupId = 5000, },
- 7. [2] = { rank = { 2, 2, }, robotGroupId = 4999, },
- 8. [3] = { rank = { 3, 3, }, robotGroupId = 4998, },
- 9. [4] = { rank = { 4, 4, }, robotGroupId = 4997, },
- 10. [5] = { rank = { 5, 5, }, robotGroupId = 4996, },
- 11. [6] = { rank = { 6, 6, }, robotGroupId = 4995, },
- 12. [7] = { rank = { 7, 7, }, robotGroupId = 4994, },
13. }
- 14.
15. do
- 16. local base = {
- 17. __index = defaultValues, --基类,默认值存取
- 18. __newindex = function()
- 19. --禁止写入新的键值
- 20. error("Attempt to modify read-only table")
- 21. end
- 22. }
- 23. for k, v in pairs(ARENA) do
- 24. setmetatable(v, base)
- 25. end
- 26. base.__metatable = false --不让外面获取到元表,防止被无意修改
27. end
- 28.
29. return ARENA
4.不要在for循环中创建表和闭包
1. local t = {1,2,3,‘hi‘}
2. for i=1,n do
- 3. --执行逻辑,但t不更改
- 4. ...
5. end
5.建议在场景切换时主动调用一次GC,包括lua、c#的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() {
- 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. {
- 6. cmpt.transform.localPosition = new Vector3(x, y, z);
7. }
- 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) {
- 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. {
- 6. Transform trans = cmpt.transform;
- 7. x = trans.localPosition.x;
- 8. y = trans.localPosition.y;
- 9. z = trans.localPosition.z;
- 10. }
- 11.
12. local pos = Vector3(0,0,0)
13. for i=1,100000 do
- 14. me.Root:SetLocalPosition(pos) --0.058831930160522
- 15. --me.Root:SetLocalPositionEx(0,0,0) -- 0.063822984695435
- 16. --me.Root:SetLocalPosition(Vector3(0,0,0)) --0.13435101509094
17. End
lua端减少频繁获取unity组件(如GetComponentFind等方法),频繁使用的组件建议缓存到本地。建议使用导出工具获取需要操作的对象,而不是find的方法,避免忘记释放导致的泄露问题。
减少在循环里通过.获取c#对象的属性,如果可以,请缓存它们。
封装方法注意点:
1. lua和c#之间传参、返回时,尽可能不要传递以下类型:
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 性能优化的主要内容,如果未能解决你的问题,请参考以下文章