【Unity WebGL】让JS比C#还快 —— Puer-webgl 性能优化实录

PuerTS是一个在Unity/Unreal里用Typescript编写逻辑的技术。

之前我为大家介绍过Puer-WebGL方案,其执行性能,开发便捷度,与宿主平台的结合效果都远强于Lua。随后我一直忙于完成PuerTS xil2cpp模式的开发,将它搁置到了一边。最近我终于有空,回来研究了一下,向大家介绍一下如何更深入地发挥Puer-WebGL的潜力,使其性能更上一个台阶。

Puer-WebGL的跨语言性能

上一篇文章中我着重介绍了Puer-WebGL方案下,JS执行性能相对于Lua的压倒性优势。但我也在官方demo中附上了一个性能测试,其中可以看到Puer-WebGL在进行跨语言的时候,是比Lua更慢的。

出现这个原因很正常:Puer-WebGL下JS是运行在宿主JS引擎的,它需要和WASM进行通信来完成对C#的调用。但Lua则本就跑在WASM里面,跨语言链路相对没那么长(具体来说就涉及到wasm通信的设计了,本文暂不表),因而,直接跨语言的话,Lua在WebGL上表现并不比JS差

但是,这并不重要,原因有两点:

  1. 实际游戏里,跨语言的消耗占比没有你想象的高

很多人喜欢在调研的时候将跨语言性能执行性能两者放在同等的重要程度上考量,但根据Puer-WebGL的实际使用者反馈,跨语言消耗占比远没有执行消耗的占比高。所以一般来说,整体效果JS还是比Lua更优。

  1. 跨语言成本可通过业务侧的设计抵消,但执行性能则是硬上限无法改变

即使你用Lua(以及任意其他脚本语言),跨语言本身也是应该尽量减少的。这个优化操作并不难,本文接下来的内容就会介绍。试想,你在开发业务时,若通过业务侧的优化,JS最终的性能上限就可以和C#在一个量级,这个感觉和被Lua禁锢在虚拟机性能上限是完全不一样的 ------ 我命由我不由天,谁不喜欢?

跨语言性能优化实录

写个demo看看原始状态

我让我们来写个简单的demo ------ 不停的生成一些方块,并且让它们每帧都往中心移动(Vector3计算)。JS与Lua都采用最简单的跨语言方式:将Vector操作的脚本函数赋值给MonoBehaviour的Delegate,并在MonoBehaviour的OnUpdate里调用该delegate(产生跨语言)。

测试结果是C#能支持5000个盒子跑50fps,Lua支持1000个盒子跑40fps,而JS在400个时就已经只剩20+fps了。和前面的跨语言测试基本一致。

C#(此时耗时大头其实是在渲染上):

Lua:

JS:

合并update

在跨语言优化中第一个可以做的是合并update。在PuerTS的官方demo上就有讲到:每帧对每个Component都进行一次C# to JS的update调用,本身的消耗是很高的。

所以我们可以在JS侧实现一个Update调度器。在js对象创建时,就往这个调度器添加update函数的回调。随后每帧只需要进行一次C# to JS update,剩下的都由JS调度即可。

经过这样的优化,Lua在WebGL下没有太大变化,但JS从原本的400盒子20+,可以变为1000盒子20+

Lua:

JS:

在JS侧实现Vector操作

优化后,我们可以看到update函数里还有大量对UnityEngine.Vector3的调用,所以你其实可以在脚本侧实现一个Vector3,其运算就直接在脚本侧进行。

这个方法也是在很早以前在xLua时期就有的办法。而JS生态完善的优势也得以体现 ------ npm上就有一个非常不错的开源数学库 math.gl,它是由Uber开发并OpenJS所赞助的,可靠性较高。

我们将运算改用js的vector后,可以看到效果也有一定提升,并且对vector3的操作越多,这个方法的效果会越明显,已经接近了Lua的效果。

但在每帧的开头和结尾还需要将数值与C#同步,仍会产生JS to C#的通信。

通过buffer操作实现C#类赋值

WASM的内存设计有一点非常不错,浏览器(或者说宿主)是允许JS通过ArrayBuffer访问所有wasm内存的。这也就意味着,如果我们在JS侧拿到了一个C#变量的指针,JS可以直接操作C#对象/结构体的属性。

通过这个办法,JS和C#同步数值时,便不需要通过常规的方法调用Vector API将向量的值设回C#了。直接内存操作即可。

如此优化后最终效果是JS能运行的盒子数达到了C#的80%(其实此时占大头的也是渲染了,逻辑运算所占比例,JS和C#差不多)

到这里为止,优化的代码我已经提交到了微信Unity小游戏转换插件仓库上作为示例

天作之合? - Puer-WebGL + Unity ECS

跟PuerTS的一位重要贡献者交流过后。他提醒我上述优化和Unity ECS模式有了许多异曲同工的地方,我深以为然。思路上,上述优化手段和ECS一样,都在一个地方统一调度所有实体的Update逻辑,都尽可能让游戏实体只用于存储数据不包含逻辑(这个是重点,它提供了跨线程友好度的同时,某种程度也提供了跨语言的友好度)。

因此,我又写了一个Unity ECS实现上述功能的demo,分别测试了纯C#以及使用Puer实现逻辑的效果。

可以看到,这个情况下JS的效率跟C#更为接近了。并且由于思路更为接近,这种写法能更容易避免不得不产生的C#调用。

但事情还没结束。

执行性能再优化 ------ 让JS比C#更快

做到上一步后,笔者就在想,有没有可能再优化一下让JS比C#更快呢?

听起来可能会有些不可思议。哪怕C#被转换成了WASM,但是要让JS比C#快,恐怕还是有些反直觉。是的,同样的环境下JS不可能比WASM里的C#更快。

但转个弯的话,是存在突破口的 ------ 上面说到Unity ECS设计很易于编写多线程逻辑,但Unity WebGL的WASM,目前还是只能单线程执行

在Unity的文档中,有一个设置可以开启多线程的支持,但根据Unity论坛里官方人员今年的描述,该多线程支持依赖于WASM Thread,该浏览器功能目前还很早期,据说对性能的优化还很有限(笔者尚未深究)。

而且浏览器标准覆盖是需要时间的,比如说WeakRef诞生至今五年,但国内iPhone仍未到达99%覆盖率。

而JS目前已经可以自如地使用Worker Thread和SharedArrayBuffer实现多线程了,浏览器已经早早地支持了它们,同时还配套加上了Atomic原子操作等辅助类。

为了测试 JS+多线程 vs WASM+单线程 的性能优劣,笔者为之前的demo加上了一个O(n^2)的碰撞检测,以增加逻辑部分相较于渲染的比重。得益于ECS的设计,这个改动并不难。

最终在浏览器里,能录得JS比WASM更快。(其实渲染的比重还是太高,否则能有更大的差距。甚至还有优化空间,比如让Unity的Heap使用SharedArrayBuffer存储的话能节省一些内存复制操作)

JS+Worker:65fps

C#: 50fps

当然,JS线程跑满,对移动设备的耗电量也许是个挑战(笔者没测试),是否真的采用JS写逻辑可以根据业务实际情况,由你们测试后再评估。但至少,借助Puer,能让你用Unity写WebGL游戏时更方便地用上线程能力。

总结

在使用Puer-WebGL时,通过减少Update时的跨语言,将逻辑移至JS,尽量使用TypedArray操作数据,可以实现全方位超越Lua的效果。同时也能更方便用上多线程API,极限情况甚至做到JS比C#更快。

上述优化的ECS部分尚没有考虑代码的封装性,所以后半部分的代码我就先不贴了。如果你的项目对上述方案有兴趣,欢迎来找我一起探索,你可以在普洱TS的官方QQ群、discord找到我

相关推荐
看到请催我学习17 分钟前
如何实现两个标签页之间的通信
javascript·css·typescript·node.js·html5
天涯学馆7 小时前
Deno与Secure TypeScript:安全的后端开发
前端·typescript·deno
applebomb10 小时前
【2024】uniapp 接入声网音频RTC【H5+Android】Unibest模板下Vue3+Typescript
typescript·uniapp·rtc·声网·unibest·agora
读心悦21 小时前
TS 中类型的继承
typescript
读心悦1 天前
在 TS 的 class 中,如何防止外部实例化
typescript
Small-K1 天前
前端框架中@路径别名原理和配置
前端·webpack·typescript·前端框架·vite
宏辉1 天前
【TypeScript】异步编程
前端·javascript·typescript
LJ小番茄2 天前
TS(type,属性修饰符,抽象类,interface)一次性全部总结
前端·javascript·vue.js·typescript
It'sMyGo3 天前
Javascript数组研究03_手写实现_fill_filter_find_findIndex_findLast_findLastIndex
前端·javascript·typescript
bobostudio19953 天前
TypeScript 算法手册【快速排序】
前端·javascript·算法·typescript