WebAssembly 在白鹭引擎5.0中的实践

做为一种可移植、体积小、加载快且兼容web的全新格式,WebAssembly受到诸多关注,并迎来企业的探索实践。白鹭引擎利用WebAssembly从新实现了一个新的渲染内核并做为一个可选项提供给开发者,使得白鹭引擎5.0成为业内首个双核驱动的引擎。在此过程当中积累了一些经验,白鹭引擎首席架构师王泽今天和你们一块儿分享背后的故事。javascript

 

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

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

在白鹭引擎 5.0 中,咱们使用 WebAssembly 从新编写了白鹭引擎的渲染核心,以便进一步提高渲染效率。在这个过程当中,白鹭引擎遇到了WebAssembly的各类问题,在此与读者进行一些 WebAssembly 在实践中遇到的问题及解决方案,但愿对计划或者正在使用WebAssembly 的开发者有所帮助。git

WebAssembly 的生成原理github

上图展现了如何经过编写 C / C++代码生成 WebAssembly 内容。web

首先经过 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 开发第一个 HelloWorld 程序。

若是您想开发 WebAssembly,强烈建议您收藏一下三个站点:

 

WebAssembly 官网:https://webassembly.org/

WebAssembly MDN:WebAssembly

Emscripten 官网:Main - Emscripten 1.37.22 documentation

 

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

 

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

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

# 获取当前版本信息

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 实践”了解更多信息。

白鹭引擎的 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 须要引入一个50k-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个显示对象在屏幕上进行旋转,其测试结果以下:

从上性能测试能够看出,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 终于获得了普遍的使用,甚至有些人认为他能够在不少场景下取 代 NativeApp ,而非仅仅是当年“取代Flash”这一小目标。凭借着底层技术的跨越式发展, 以及浏览器厂商的一致支持,WebAssembly必定会有一个光明的将来。

相关文章
相关标签/搜索