返回 登录
1

WebAssembly在白鹭引擎5.0中的实践

作者:王泽,北京白鹭时代信息技术有限公司白鹭引擎首席架构师。目前主要聚焦于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的生成原理

图片描述

图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。

图片描述

表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前端开发者”群,与更多专家、技术同行进行热点、难点技术交流。请扫描以下二维码申请入群。
图片描述

评论