http://mp.weixin.qq.com/s?__biz=MzI3MzA2MzE5Nw==&mid=2668904614&idx=1&sn=cbf0647bd5458051e20354bb2ed1042f&chksm=f1c9ecd4c6be65c2c7fe2f30e078090a20b67d0c5ab380dc1c7145a4c7da4d1a50fd08fad788&mpshare=1&scene=2&srcid=1103JMOeRXAFM6qnJWw1qJVy&from=timeline&isappinstalled=0#wechat_redirect
整合Lua是目前最强大的Unity热更新方案,毕竟这是唯一可以支持iOS热更新的办法。然而作为一个重度uLua用户,我们踩过了很多坑才将uLua上升到一个可以在项目中大规模使用的状态。事实上即使到现在,Lua+Unity的方案仍不能肆意地被使用。要用好,你需要知道很多。在看了UWA之前发布的《Unity项目常见Lua解决方案性能比较》一文后,笔者决定动手写一下关于Lua+Unity方案的性能优化技巧。
感谢来自深圳游戏科学的招文勇供稿(QQ: 1490582806,博客:http://www.cnblogs.com/zwywilliam/)。欢迎转发分享,未经作者授权请勿转载。如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(465082844)
同时,作者也是U Sparkle活动参与者哦,UWA欢迎更多开发朋友加入 U Sparkle开发者计划,这个舞台有你更精彩!
◆◆◆
启
从最早的Lua纯反射调用C#,以及云风团队尝试的纯C#实现的Lua虚拟机,一直发展到现在的各种Luajit+C#静态Lua导出方案,Lua+Unity才算达到了性能上实用的级别。但即使这样,实际使用中我们会发现,比起Cocos2dx时代Luajit的发扬光大,现在Lua+Unity的性能依然存在着相当大的瓶颈。仅从《Unity项目常见Lua解决方案性能比较》的test1就可以看到,iPhone 4S下二十万次Position赋值就已经需要3000ms,如果是COC这样类型的游戏,不处理其他逻辑,一帧仅仅上千次位置赋值(比如数百的单位、特效和血条)就需要15ms,这显然有些偏高。是什么导致Lua+Unity的性能并未达到极致,要如何才能更好地使用?我们将结合一些例子逐步挖掘其背后的细节。
由于我们项目主要使用的是uLua(集成了topameng的CsToLua,但是由于持续的性能改进,后面已经做过大量的修改),本文的大部分结论都是基于uLua+CsToLua的测试得出来的,sLua都是基于其源码来分析(根据我们分析的情况来看,两者原理上基本一致,仅在实现细节上有一些区别),但没有做过深入测试,如有问题的话欢迎交流。
既然是Lua+Unity,那性能好不好,基本上要看两大点:
1、Lua跟C#交互时的性能如何
2、纯Lua代码本身的性能如何
因为这两部分都有各自需要深入探讨的细节,所以我们会分为多篇去探讨整个Lua+Unity到底如何进行优化。
◆◆◆
Lua与C#交互篇
一、 从致命的gameobj.transform.position = pos说起
像gameobj.transform.position = pos这样的写法,在Unity中是再常见不过的事情。但是在uLua中,大量使用这种写法是非常糟糕的。为什么呢?
因为短短一行代码,却发生了非常非常多的事情,为了更直观一点,我们把这行代码调用过的关键Lua API以及uLua相关的关键步骤列出来(以uLua+CsToLua导出为准,gameobj是GameObject类型,pos是Vector3):
就这么一行代码,竟然做了这么一大堆的事情!如果是C++,a.b.c = x这样经过优化后无非就是拿地址然后内存赋值的事。但是在这里,频繁的取值、入栈、C#到Lua的类型转换,每一步都是满满的CPU时间,还不考虑中间产生了各种内存分配和后面的GC!
下面我们会逐步说明,其中有一些东西其实是不必要的,可以省略的。我们可以最终把它优化成:lua_isnumber + lua_tonumber 4次,全部完成。
二、在Lua中引用C#的Object,代价昂贵
从上面的例子可以看到,仅仅想从gameobj拿到一个transform,就已经有很昂贵的代价。C#的Object,不能作为指针直接供c操作(其实可以通过GCHandle进行pinning来做到,不过性能如何未测试,而且被pinning的对象无法用GC管理),因此主流的Lua+Unity都是用一个ID表示C#的对象,在C#中通过dictionary来对应ID和object。同时因为有了这个dictionary的引用,也保证了C#的object在Lua有引用的情况下不会被垃圾回收掉。
因此,每次参数中带有object,要从Lua中的ID表示转换回C#的object,就要做一次dictionary查找;每次调用一个object的成员方法,也要先找到这个object,也就要做dictionary查找。
如果之前这个对象在Lua中有用过而且没被GC,那还就是查下dictionary的事情。但如果发现是一个新的在Lua中没用过的对象,那就是上面例子中那一大串的准备工作了。
如果你返回的对象只是临时在Lua中用一下,情况更糟糕!刚分配的userdata和dictionary索引可能会因为Lua的引用被GC而删除掉,然后下次你用到这个对象又得再次做各种准备工作,导致反复的分配和GC,性能很差。
例子中的gameobj.transform就是一个巨大的陷阱,因为.transform只是临时返回一下,但是你后面根本没引用,又会很快被Lua释放掉,导致你后面每次.transform一次,都可能意味着一次分配和GC。
三、在Lua和C#间传递Unity独有的值类型(Vector3/Quaternion等)更加昂贵
既然前面说了Lua调用C#对象缓慢,如果每次vector3.x都要经过C#,那性能基本上就处于崩溃了,所以主流的方案都将Vector3等类型实现为纯Lua代码,Vector3就是一个{x,y,z}的table,这样在Lua中使用就快了。
但是这样做之后,C#和Lua中对Vector3的表示就完全是两个东西了,所以传参就涉及到Lua类型和C#类型的转换,例如C#将Vector3传给Lua,整个流程如下:
1. C#中拿到Vector3的x、y、z三个值;
2. Push这3个float给Lua栈;
3. 然后构造一个表,将表的x,y,z赋值;
4. 将这个表push到返回值里。
一个简单的传参就要完成3次push参数、表内存分配、3次表插入,性能可想而知。
那么如何优化呢?我们的测试表明,直接在函数中传递三个float,要比传递Vector3要更快。例如void SetPos(GameObject obj, Vector3 pos)改为void SetPos(GameObject obj, float x, float y, float z)。具体效果可以看后面的测试数据,提升十分明显。
四、Lua和C#之间传参、返回时,尽可能不要传递以下类型:
严重类: Vector3/Quaternion等Unity值类型,数组
次严重类:bool string 各种object
建议传递:int float double
虽然是Lua和C#的传参,但是从传参这个角度讲,Lua和C#中间其实还夹着一层C(毕竟Lua本身也是C实现的),Lua、C、C#由于在很多数据类型的表示以及内存分配策略都不同,因此这些数据在三者间传递,往往需要进行转换(术语parameter mashalling),这个转换消耗根据不同的类型会有很大的不同。
先说次严重类中的 bool和 string类型,涉及到C和C#的交互性能消耗,根据微软官方文档,在数据类型的处理上,C#定义了Blittable Types和Non-Blittable Types,其中bool和string属于Non-Blittable Types,意思是他们在C和C#中的内存表示不一样,意味着从C传递到C#时需要进行类型转换,降低性能,而string还要考虑内存分配(将string的内存复制到托管堆,以及utf8和utf16互转)。大家可以参考https://msdn.microsoft.com/zh-cn/library/ms998551.aspx,这里有更详细的关于C和C#交互的性能优化指引。
而严重类,基本上是uLua等方案在尝试Lua对象与C#对象对应时的瓶颈所致。Vector3等值类型的消耗,前面已经有所提及。
而数组则更甚,因为Lua中的数组只能以table表示,这和C#下完全是两码事,没有直接的对应关系,因此从C#的数组转换为Lua table只能逐个复制,如果涉及object/string等,更是要逐个转换。
五、频繁调用的函数,参数的数量要控制
无论是Lua的pushint/checkint,还是C到C#的参数传递,参数转换都是最主要的消耗,而且是逐个参数进行的,因此,Lua调用C#的性能,除了跟参数类型相关外,也跟参数个数有很大关系。一般而言,频繁调用的函数不要超过4个参数,而动辄十几个参数的函数如果频繁调用,你会看到很明显的性能下降,手机上可能一帧调用数百次就可以看到10ms级别的时间。
六、优先使用static函数导出,减少使用成员方法导出
前面提到,一个object要访问成员方法或者成员变量,都需要查找Lua userdata和C#对象的引用,或者查找metatable,耗时甚多。直接导出static函数,可以减少这样的消耗。
像obj.transform.position = pos。我们建议的方法是,写成静态导出函数,类似
class LuaUtil{
static void SetPos(GameObject obj, float x, float y, float z){obj.transform.position = new Vector3(x, y, z); }
}
然后在Lua中LuaUtil.SetPos(obj, pos.x, pos.y, pos.z),这样的性能会好非常多,因为省掉了transform的频繁返回,而且还避免了transform经常临时返回引起Lua的GC。
七、注意Lua拿着C#对象的引用时会造成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中只使用自己管理的ID,而不直接引用C#的Object
想避免Lua引用C# Object带来的各种性能问题的其中一个方法就是自己分配ID去索引Object,同时相关C#导出函数不再传递Object做参数,而是传递int。这带来几个好处:
1. 函数调用的性能更好;
2. 明确地管理这些Object的生命周期,避免让ULua自动管理这些对象的引用,如果在Lua中错误地引用了这些对象会导致对象无法释放,从而内存泄露;
3. C#Object返回到Lua中,如果Lua没有引用,又会很容易马上GC,并且删除ObjectTranslator对Object的引用。自行管理这个引用关系,就不会频繁发生这样的GC行为和分配行为。
例如,上面的LuaUtil.SetPos(GameObject obj, float x, float y, float z)可以进一步优化为LuaUtil.SetPos(int objID, float x, float y, float z)。然后我们在自己的代码里头记录objID跟GameObject的对应关系,如果可以,用数组来记录而不是dictionary,则会有更快的查找效率。如此下来可以进一步省掉Lua调用C#的时间,并且对象的管理也会更高效。
九、合理利用out关键字返回复杂的返回值
在C#向Lua返回各种类型的东西跟传参类似,也是有各种消耗的。比如 Vector3 GetPos(GameObject obj) 可以写成 void GetPos(GameObject obj, out float x, out float y, out float z)。表面上参数个数增多了,但是根据生成出来的导出代码(我们以uLua为准),会从:LuaDLL.tolua_getfloat3(内含get_field + tonumber 3次) 变成 isnumber + tonumber 3次。get_field本质上是表查找,肯定比isnumber访问栈更慢,因此这样做会有更好的性能。
◆◆◆
实测
好了,说了这么多,不拿点数据来看还是太晦涩,为了更真实地看到纯语言本身的消耗,我们直接没有使用例子中的gameobj.transform.position,因为这里头有一部分时间是浪费在Unity内部的。
我们重写了一个简化版的GameObject2和Transform2。
class Transform2{
public Vector3 position = new Vector3();
}
class GameObject2{
public Transform2 transform = new Transform2();
}
然后我们用几个不同的调用方式来设置transform的position
方式1:gameobject.transform.position = Vector3.New(1,2,3)
方式2:gameobject:SetPos(Vector3.New(1,2,3))
方式3:gameobject:SetPos2(1,2,3)
方式4:GOUtil.SetPos(gameobject, Vector3.New(1,2,3))
方式5:GOUtil.SetPos2(gameobjectid, Vector3.New(1,2,3))
方式6:GOUtil.SetPos3(gameobjectid, 1,2,3)
分别进行100万次,结果如下(测试环境是Windows版本,CPU是i7-4770,luajit的jit模式关闭,手机上会因为luajit架构、IL2CPP等因素干扰有所不同):
方式1:903ms
方式2:539ms
方式3:343ms
方式4:559ms
方式5:470ms
方式6:304ms
可以看到,每一步优化,都是提升明显的,尤其是移除.transform获取以及Vector3转换提升更是巨大,我们仅仅只是改变了对外导出的方式,并不需要付出很高成本,就已经可以节省66%的时间。
实际上能不能再进一步呢?还能!在方式6的基础上,我们可以再做到只有200ms!这里卖个关子,我们将在luajit集成中进行进一步讲解。一般来说,我们推荐做到方式6的水平已经足够。
这只是一个最简单的案例,有很多各种各样的常用导出(例如GetComponentsInChildren这种性能大坑,或者一个函数传递十几个参数的情况)都需要大家根据自己使用的情况来进行优化,有了我们提供的Lua集成方案背后的性能原理分析,应该就很容易去考虑怎么做了。
附测试用例的C#代码
文末,再次感谢招文勇的分享,如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨
分享到:
相关推荐
在IT行业中,开发环境的配置和工具的合理...通过以上步骤,你将能够熟练地在VSCode中使用Unity、EmmyLua和C#进行开发和调试,极大地提升你的开发效率。记住,不断学习和适应新的工具和技巧是IT行业不可或缺的一部分。
本文将深入探讨如何使用lua++库实现C++与Lua的无缝交互。lua++是一个C++与Lua绑定的工具,它允许C++代码调用Lua脚本,同时也使Lua能够调用C++函数,从而实现两者之间的高效通信。 首先,让我们理解lua++的基本概念...
EmmyLua-Unity 是一个专为Unity游戏引擎开发者设计的工具,它是由国内开发者创建的,目的是提升在Unity中编写Lua脚本的效率和便捷性。这个插件主要与IntelliJ IDEA集成,IntelliJ IDEA是一款强大的Java开发集成环境...
OpenResty 是基于 Nginx 的高性能 web 服务器,它使用 Lua 语言作为脚本语言来实现动态内容生成和处理。 Lua 是一种轻量级的脚本语言,广泛应用于游戏、嵌入式系统和 web 开发等领域。Redis 是一种高性能的 NoSQL ...
总结起来,"C#与Lua交互的例程"是关于使用LuaInterface库实现C#与Lua的互操作性的一个实例教程。通过这个例程,开发者可以学习如何在C#中调用Lua脚本,以及如何在Lua中调用C#的方法。这对于那些需要在C#应用中集成...
在这个“nginx+lua+redis集群”解决方案中,提供的连接插件可能是指Nginx与Redis之间的通信模块,例如lua-redis-pconnector,这个插件允许Nginx通过Lua脚本直接与Redis进行交互。而脚本部分可能包括了处理业务逻辑、...
在`nginx`中,我们可以使用`lua`模块来处理这些逻辑。`lua`脚本可以直接嵌入到`nginx`配置中,允许我们在不重启服务器的情况下动态执行代码。例如,我们可以编写一个`lua`脚本来从请求头中提取`token`,然后使用`...
Unity本身并不直接支持LUA代码的集成和调试,但可以通过第三方库如XLUA进行桥接,将C#与LUA结合使用。XLUA是一个强大的工具,它允许开发者在Unity项目中无缝地集成LUA脚本,同时提供了C#和LUA之间的互操作性。 XLUA...
"EmmyLuaService.cs"可能是这个环境在Unity中的服务类,用于在C#与Lua之间进行交互和桥接。 "IntelliJ-EmmyLua-1.3.4.157-IDEA202.zip"是EmmyLua插件的安装包,适用于IntelliJ IDEA 202版本。安装该插件后,开发者...
标题"“LUA与C#2010交互引用LUA库”"指出,我们将探讨如何在C# 2010环境中使用LUA库来实现两种语言之间的交互。这种交互通常涉及到将C#的业务逻辑暴露给LUA脚本,或者从LUA脚本调用C#的函数。 描述中提到的"包括一...
标题 "lua+c++ vs2013" 涉及的是在Visual Studio 2013环境下使用C++调用Lua脚本的技术。Lua是一种轻量级的脚本语言,常用于游戏开发、配置文件处理等领域,而C++则是一种强大且广泛应用的系统编程语言。将两者结合...
nginx+lua+docker+docker-compose实现简单服务分发
nginx+lua+redis通过匹配客户端ip进行灰度发布 本文将讲述如何使用nginx、lua和redis来实现灰度发布,通过匹配客户端IP来实现灰度发布。灰度发布是一种常见的软件发布方式,它允许开发者在生产环境中发布新的版本,...
此外,`Unity新版本API提示.mp4`和`新版Unity API提示使用说明.txt`是辅助学习资源,可以帮助开发者更好地理解和使用Unity的新API,提升开发效率。 通过以上步骤,你就能在Unity中成功地安装和配置EmmyLua环境,...
在本案例中,我们讨论的是"unity xlua lua5.4.4最新版lua库文件",这意味着你已经获取了最新的Lua版本5.4.4与Unity结合使用的资源包。 Lua是一种轻量级的嵌入式脚本语言,设计简洁且易于学习。它的语法简洁明了,...
在IT领域,LUA(Lightweight ...通过以上知识点的运用,开发者可以在C# 2010项目中成功地引入并使用LUA库,实现C#与LUA之间的高效交互。在实际开发中,还需要根据具体需求进行适当的调整和优化,以实现最佳的集成效果。
Nginx+lua+OpenResty高性能实践 高性能 处理高并发能力上很强 高扩展性 Nginx模块化,官方提供了多元化的模块。 高可用性 三个9 99.9% 四个9 99.99% 热部署(版本平滑升级) 服务进行升级的同时,保证业务 不宕机 ...
2. Lua脚本编写:然后,开发者可以在Lua环境中编写游戏逻辑,利用xlua提供的API与Unity的C#组件进行交互。 3. 绑定C#类:使用xlua的工具,如tolua++,将Unity的C#类绑定到Lua,这样Lua就能直接操作Unity的对象。 4...
Nginx+Lua+GraphicsMagick实现动态生成指定尺寸的图片
"使用Tengine+Lua+GraphicsMagick实现图片自动裁剪缩放" 在互联网快速发展的今天,图片数量的增长对网站的性能和用户体验产生了很大的影响。为了解决图片数量的增长带来的问题,我们需要实现图片的自动裁剪和缩放,...