欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

WebAssembly在白鹭引擎5.0中的实践

程序员文章站 2024-03-20 19:51:52
...

作者:王泽,北京白鹭时代信息技术有限公司白鹭引擎首席架构师。目前主要聚焦于HTML5游戏引擎开发、TypeScript以及WebAssembly技术相关的研究与实践工作。
责编:陈秋歌,关注前端开发领域,寻求报道或者投稿请发邮件chenqg#csdn.net。
本文为《程序员》原创文章,未经允许不得转载,更多精彩文章请订阅《程序员》

导读:作为一种可移植、体积小、加载快且兼容Web的全新格式,WebAssembly受到诸多关注,并迎来企业的探索实践。白鹭引擎5.0利用它重新编写了渲染核心,此过程中,遇到了很多问题。本文将针对这些问题,分享背后的解决方案。

WebAssembly是Google Chrome、Mozilla FireFox、Microsoft Edge、Mozilla FireFox共同宣布支持,并在2017年3月份在各自浏览器中提供了实现的一种新技术。它被设计为一种可移植的、安全的、低尺寸的、高效的二进制格式。浏览器可以解析并运行这种格式,并拥有比JavaScript更高的性能和解析速度。WebAssembly可以通过编写C/C++代码,通过专门的编译器生成.wasm格式的文件,直接运行在最新的浏览器中。

白鹭引擎是一款HTML5游戏引擎,提供了游戏开发所需要的诸多功能,并允许开发者编写的游戏运行在Web浏览器或移动应用的WebView容器中。

在白鹭引擎5.0中,我们使用WebAssembly重新编写了白鹭引擎的渲染核心,以便进一步提升渲染效率。在这个过程中,白鹭引擎遇到了WebAssembly的各种问题,在此与读者分享一些WebAssembly在实践中遇到的问题及解决方案,希望对计划或者正在使用WebAssembly的开发者有所帮助。

WebAssembly的生成原理

WebAssembly在白鹭引擎5.0中的实践

图1 C/C++代码被编译为WebAssembly代码的过程

图1展示了讲C/C++代码编译成WebAssembly内容的过程。

首先通过LLVM,将C/C++源代码编译为LLVM bytecode。这是一种跨语言的底层虚拟机字节码,理论上所有强类型编程语言均可以生成这种字节码。通过这一点可以得知,在未来理论上所有强类型编程语言(诸如Java、C#等)均可以开发WebAssembly程序。

其次,通过Emscripten中的后端编译器,将这种抽象字节码生成asm.js格式的文件。这是一种特殊的JavaScript代码,一些JavaScript引擎会将这种格式以比通常JavaScript代码更快的速度运行,并且由于asm.js仍然是JavaScript,所以哪怕JavaScript引擎不支持该特性,也会以通常的方式运行这段逻辑。这意味着使用C/C++编写的源代码,哪怕用户设备不支持WebAssembly,也可以回退到JavaScript运行并得到一致的结果。

接下来,asm.js会通过另一个编译器生成为WebAssembly的.wasm文件。WebAssembly 是二进制格式,相比JavaScript而言,其代码体积同比小很多,并且已经是面向机器码的格式,也无需在运行前对源代码耗费时间进行JIT编译操作。

通过上述内容可以看出,WebAssembly理论上可以通过任何强类型语言生成,不强制依赖用户的本地运行环境,代码体积小、解析速度快,几乎彻底解决了JavaScript的各种顽疾。

WebAssmely简单入门

开发环境配置

如果您想开发 WebAssembly,强烈建议您收藏以下三个站点。

在具体的开发中遇到的问题,大部分可在这三个网站中找到答案。

首先,进行项目开发前需要配置WebAssembly开发环境。本文以 Windows 为例,MacOS 与 Linux开发者可以阅读Emscripten官网文档。

在Windows中,可以直接从Emscripten官网下载Emscripten SDK,安装后,在命令行输入 emcc -v,可以看到显示当前版本号为 1.35.0。为了保证最佳的开发体验,我们需要执行以下命令,手动升级Emscripten SDK到最新版本,。

# 获取当前版本信息
emsdk update 
# 安装最新版本,笔者目前为 1.37.14
emsdk install latest
# 使用最新版本
emsdk activate latest

在安装过程中,需要下载文件,考虑到国内的特殊网络环境,有时下载会失败,你可以根据下载时的日志输出,提前将要下载的文件放置于正确路径,然后再执行安装命令。

编写HelloWorld应用

在保证Emscripten处于最新版本后,就可以开始编写HelloWorld应用了。

创建一个新的C文件,名为main.c,编写以下内容。

#include <stdio.h>

int main() {
printf("hello, world!\n");
return 0;
}

然后在终端中执行以下命令emcc main.c -o out/index.html最终会生成以下项目结构。

project-root
|-- main.c
|-- out/index.html
|-- out/index.js

你应该已经发现,生成的代码并不包含WebAssembly的wasm格式文件,而是一个名为 index.js的asm.js文件。这是因为Emscripten最初是为了生成asm.js格式而设计的。为了生成wasm,需要额外添加一个参数emcc main.c -o out/index.html -s WASM=1,当添加这个参数后,Emscripten会再通过一个名为Binaryen的编译器将asm.js格式转换为wasm格式。

细心的你可能会发现,理论上Binaryen无需asm.js这个中间格式,而应该从C++生成的LLVM直接输出wasm格式,目前Binaryen已经支持了这种方式,但是目前还在测试阶段,所以默认行为仍然是通过asm.js作为中间层。

添加完上述参数后重新执行,就会发现项目中生成了名为index.wasm的文件,运行index.html,可以看到屏幕上输出了“Hello,World”。

与JavaScript进行交互

除了标准C之外,Emscripten提供了大量函数,用于JavaScript、HTML与WebAssembly进行通讯,其最简单的代码如下所示。

#include <emscripten.h>

int main() {
  EM_ASM( alert("hai"));
return 0;
}

通过引入emscripten.h头文件,就可以调用这些函数,上述代码中展示了如何在 WebAssembly中直接调用JavaScript内容。

为了简化调用,Emscripten提供了EMSCRIPTEN_BINDING等API,可以将一个C++类和函数与JavaScript进行直接绑定。

由于WebAssembly与JavaScript的调用存在着一定的性能问题,所以更推荐开发者使用typed_memory_view的方式,将WebAssembly中的一段内存与JavaScript的一段TypedArray进行绑定,通过这种方式,WebAssembly与JavaScript的调用不是通过拷贝数据,而是直接以对内存进行共享的方式进行交互。通过灵活运用这种方式,可以大幅提升性能,一些更为具体的实际案例将在下文进行展示。

白鹭引擎的WebAssembly实践

在网页端运行一款游戏的几种方式

通过浏览器插件机制,在网页插件中运行游戏,如Flash Player、Unity Web Player等。这种机制的优势是由于插件本身使用NativeCode对游戏组件进行了许多封装,所以运行效率很高,缺点则是需要浏览器支持,而现在浏览器更加倾向于无插件化。

其次是游戏逻辑和游戏引擎均交由JavaScript进行处理,最终渲染则通过控制DOM节点或者操作DOM Canvas相关API去实现。这种方式实现了无插件化,但是由于JavaScript自身性能存在瓶颈,性能也有一定的局限性。目前市面上绝大多数HTML5游戏引擎(包括白鹭引擎)均是如此实现,扩展到WebApp开发行业,无论是Angular、React还是其他诸多框架的核心架构也是如此。

由于WebAssembly的引入,一些大型游戏引擎厂商,比如Unity3D,开始尝试将其游戏源代码编译为WebAssembly,运行在浏览器中,这种做法理论上可以把大量基于C/C++编写的游戏发布为HTML5版本。但由于HTML5游戏本身的资源加载机制与客户端游戏完全不同,直接转换的游戏仍然需要改造很多逻辑去适应网页端“边加载边进行游戏”的需求,否则当用户进入游戏时,需要加载上百兆的游戏资源才能进入游戏,这带来了极其糟糕的体验,并且很占用内存。

由于将整个客户端游戏直接发布为WebAssembly格式目前并不成熟,所以我们认为把游戏中性能消耗较大的部分转为WebAssembly,而将需要强调开发效率的部分继续使用JavaScript是一种灵活的方式。

在上述四种方案中,主要是后两种采用了WebAssembly技术。在目前来看,由于第四种方案较为稳妥,所以白鹭引擎采用了这种方案,在最新版本5.0中提供了基于WebAssembly的渲染内核,而游戏逻辑本身仍然运行在JavaScript环境中。

JavaScript与WebAssembly互操作性能很差

以白鹭引擎5.0的渲染库为例,白鹭引擎对外提供JavaScript API,开发者编写的JavaScript逻辑代码会汇总为一组命令队列发送给WebAssembly层,然后WebAssembly建立对渲染节点的抽象封装,并在每一帧对这些渲染节点进行矩阵计算、渲染命令生成等逻辑,最终生成一组ArrayBuffer数据流,最后JavaScript对这组数据流进行简单的解析并直接调用DOM的WebGL接口,把数据流传递给浏览器层。

这个过程中存在着几个性能瓶颈。

首先,JavaScript与WebAssembly的对象绑定后,互相调用的性能很差,这大大限制了WebAssembly的适用范围。简单地将特定几个函数编译为WebAssembly,然后交由JavaScript去调用反而会因为频繁的互相操作造成性能下降。为了绕过这个问题,WebAssembly设计了一组API,可以用于将一段JavaScript ArrayBuffer与WebAssemly中的字节流进行共享操作。所以白鹭引擎将所有对WebAssembly的调用封装为一组字节流命令,并在用户逻辑全部执行完之后,将这个字节流命令传递给WebAssembly,这样就大幅减少了JavaScript和WebAssembly之间的互操作。

其次,WebAssembly不能直接操作WebGL等浏览器API,所以在每一帧对渲染内容完成计算之后,需要把计算结果再保存在一段字节流中,共享给JavaScript,交由JavaScript去操作DOM节点。由于最终仍然是JavaScript去操作DOM节点,必然仍存在一定的性能问题。无法操作DOM节点使得目前WebAssembly无法完全代替JavaScript。这一问题在WebAssembly的路线图中有所提及,会在未来的版本中加以解决。

因此可以看出,WebAssembly适合将一段大量的、密集的逻辑计算抽象出来,统一一次性输入所有的参数、一次性返回所有的输出,比如游戏主渲染循环、物理引擎、粒子系统、骨骼动画计算等内容。

WebAssembly的二进制格式可调试性较差

WebAssembly被设计为一种开放的、可调试的程序,但目前无论是Chrome还是FireFox,在调试方面还有很大的提升空间。由于在目前阶段调试较为困难,所以用WebAssembly编写业务逻辑代码对研发来说还是很不方便的。目前白鹭引擎的策略是把Emscripten中的API与业务逻辑进行隔离,通过C++自身的开发环境,剥离Emscripten进行独立的调试,然后再发布为WebAssembly格式,而非直接在浏览器端调试WebAssembly。

虽然目前可调试性较差,但我们相信这个问题在未来一定会得到较好的解决。同时,由于二进制的原因,代码体积很小,白鹭引擎团队将大约300k左右(压缩后)JavaScript逻辑改用WebAssembly重写后,体积仅有90k左右。虽然使用WebAssembly需要引入一个50-100k的JavaScript类库作为基础设施,但是总体来看资源尺寸的优势还是很大的。

由于代码格式是二进制,无法直接在浏览器中看到源码,尽管理论上仍然可以通过****,在一定程度上得到原有的业务逻辑,但由于开发者可以在编译时使用-O3等激进的优化策略,所以最终反编译得到的业务逻辑也是很难阅读的。虽然理论上一切在客户端的内容都是不安全的,但是与所有代码都直接暴露给用户相比,代码安全性得到了很大的改善。

WebAssembly的浏览器支持率仍很低

在当前,Chrome 57+(包括PC与Android),iOS 11 Safari、FireFox 52与Microsoft Edge均已支持WebAssembly,但仍然存在不稳定现象。以Chrome浏览器为例,Chrome 57支持WebAssembly的MVP版本,但是在Chrome 58上,大量的WebAssembly程序会直接导致进程崩溃,虽然后续的Chrome 59已经修复了绝大部分问题,但是仍然不得不对目前版本的稳定性持保留态度。

在不支持WebAssembly的浏览器中,由于C++代码在编译WebAssemly的同时也可以编译出完全符合JavaScript语法的asm.js,所以保证业务逻辑是可以通过这种方式回退支持所有的浏览器。

WebAssembly在移动设备上性能并没有跨越式提升

经过测试发现,在PC Chrome上,WebAssembly相比JavaScript的性能有很大提升,但是在Mobile Chrome上,提升目前只有30%左右,这说明目前WebAssembly在性能挖掘上还有很大空间。

我运行了一个复杂的测试用例,15000个显示对象在屏幕上进行旋转,其测试结果见表1。

WebAssembly在白鹭引擎5.0中的实践

表1 性能测试结果

通过性能测试可以看出,WebAssembly比JavaScript版本以及asm.js版本均有一定提升。由于在测试Demo中,游戏逻辑(每一帧遍历15000个显示对象,修改其旋转属性)无论在任何版本中均处于JavaScript环境运行。所以游戏逻辑的开销在三个版本中是一致的,而使用WebAssembly实现的渲染逻辑比JavaScript版本快30%以上。

在运行benchmark等极限测试时,游戏引擎使用WebAssembly并不比JavaScript有成倍的提升。我的推论是:由于JavaScript引擎的JIT机制会把经常运行的函数进行极限的编译优化,所以在benchmark这种代码大量反复执行的测试环境下,无论是JavaScript版本,还是WebAssembly版本,运行的都是高度优化后的机器码。虽然WebAssembly版本仍然比JavaScript版有一定的性能优势,但是并不明显。而在运行业务逻辑代码时,由于大部分业务逻辑代码只运行一次,所以JavaScript引擎只会对这部分代码进行简单的编译优化而非极限优化,所以运行这一部分代码WebAssembly相比JavaScript版本而言提升巨大。不过,正如上文所述,不建议开发者在编写业务逻辑时使用WebAssembly,所以这里陷入了一个两难境地。在目前而言,理想情况是除了底层库之外,部分关键的涉及性能问题的逻辑也可以使用WebAssembly进行编写。

结论

综上所述,目前为止由于WebAssembly还不是非常完善,所以它目前的主要作用是作为JavaScript生态的有益补充,与JavaScript共存而不是取而代之。但是通过其路线图我们可以得知,WebAssembly的设计思想非常优秀,目前所有存在的问题从长远的角度来说都是可以解决的问题。在加上WebAssembly是非常罕见的由四大浏览器厂商共同宣布会大力支持并实现的功能,其浏览器兼容性问题也终究可以得到解决,再退一步,哪怕旧式浏览器不支持,由于WebAssembly支持回退到JavaScript,也可以保证正常运行。

我认为,WebAssembly就像当初的HTML5标准一样,在公布之后最开始不被很多人看好,认为会有浏览器兼容性问题、各大浏览器厂商的实现问题、性能问题、用户需求与用户体验问题,但在近年来HTML5终于得到了广泛的使用,甚至有些人认为它可以在很多场景下取代Native App,而非仅仅是当年“取代Flash”这一小目标。凭借着底层技术的跨越式发展,以及浏览器厂商的一致支持,WebAssembly一定会有一个光明的未来。

欢迎加入“CSDN前端开发者”群,与更多专家、技术同行进行热点、难点技术交流。请扫描以下二维码申请入群。
WebAssembly在白鹭引擎5.0中的实践

上一篇: PHP教程之四 php与数据库

下一篇: