WebAssembly

Maker Su
Maker Su
Jul 24, 2017 · 8 min read

Google V8 引擎是 JavaScript 引擎性能改善的现象级项目。V8 于 2007 年发布,由于采用了 JIT ( just-in-time)技术,V8 引擎可以将 JavaScript 代码在运行前编译为机器语言,这样运行速度就会有大幅提升。

首先是被称为“重优化”的性能瓶颈。编译为机器代码的前提条件是必须让编译器清晰的知道变量的类型,偏偏 JavaScript 是一门弱类型语言,参考如下代码:

function sum ( a , b ) {
return a + b;
}
sum(1,2);
sum("1","2");

其次是因为编译器的一些优化策略可能“弄巧成拙”,导致在特定情况下性能反而有负面影响。(https://github.com/Microsoft/TypeScript/pull/10270)

再次是 JavaScript 具备垃圾回收机制,虽然 V8 编译器已经对垃圾回收机制的算法进行了诸多的优化,但是在应用内存占用较大时,垃圾回收的瞬间明显仍然还有卡顿现象,这导致了复杂应用有可能出现不定时的卡顿现象。

这些问题都反映出,JavaScript 这种语言机制本身的灵活性,反而限制了 JavaScript 引擎的性能优化空间,如果希望彻底解决这一问题,必然需要抛弃 JavaScript 这门语言本身,采用一门强类型的编程语言才能达到最极致的性能,在这种技术思想的指引下,WebAssembly 技术应运而生。


WebAssembly 是一种接近机器语言的跨平台二进制格式。2017 年 3 月份,四大主流浏览器厂商 Google Chrome、Apple Safari、Microsoft Edge 和 Mozilla FireFox 均宣布已经于最新版本的浏览器中支持了 WebAssembly 的初始版本,这意味着 WebAssembly 技术已经实际落地、可以在特定生产环境进行尝试。

提到了 WebAssembly,就必然首先提及对其有深远影响的 asm.js,这是 Mozilla 在 2013 年推出的一项新技术,它是 JavaScript 的一个子集,舍弃了大量会导致性能问题的语法,并且被设计为通过 C / C++ 代码编译生成,而非手工编写 asm.js 代码。上述的 sum 函数在 asm.js 中表现为:

function sum ( a ,b ) {
a = a | 0;
b = b | 0;
return ( a + b ) | 0;
}

通过这种方式,这种代码既可以在支持 asm.js 的 JavaScript 引擎上得到很高的性能,也会在不支持的设备上继续按照正确的逻辑进行执行,而非无法运行。虽然如此,asm.js 仍然存在着一些问题,主要是基于 JavaScript 语法的文本格式解析速度不够快,并且代码尺寸偏大,为了解决这些问题,将 asm.js 进行二进制化的 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 理论上可以通过任何强类型语言生成,不强制依赖用户的本地运行环境,代码体积小、解析速度快,几乎是 Web 开发未来的一颗“银色子弹”。


可惜的是在现阶段,WebAssembly 仍然存在着不少问题需要去解决。

首先是自身的稳定性,以 Chrome 浏览器为例,Chrome 57 支持 WebAssembly 的 MVP 版本,但是在 Chrome 58 上,大量的 WebAssembly 程序会直接导致进程崩溃,虽然后续的 Chrome 59 已经修复了绝大部分问题,但是仍然不得不对目前版本的稳定性持保留态度。

其次是可调试性,WebAssembly 被设计为了一种开放的、可调试的程序,但目前无论是 Chrome 还是 FireFox ,在调试方面还有很大的提升空间。由于在目前阶段调试较为困难,所以用 WebAssembly 编写业务逻辑代码对研发来说还是很不方便的。

还有就是与 Web 的互操作性。目前 WebAssembly 类似 WebWorker ,只能进行单纯的数值计算工作,不能在 C++ 层直接操作 DOM 节点。虽然在未来路线图中提及这一特性会在后续加入,但是在目前阶段 WebAssembly 更适合被用于更纯粹的密集型数据计算工作,而非直接编写业务逻辑

综上所述,在目前阶段,WebAssembly 不适合直接编写具体的业务逻辑,而更适合编写应用程序中对性能要求比较高的库,并与 JavaScript 编写的业务逻辑进行通讯,并在 JavaScript 端对 DOM 节点进行操作。


在实践过程中,我们总结出 WebAssembly 的几个不容易注意的优势和缺点:

代码体积很小,我们将大约 300k 左右(压缩后)JavaScript 逻辑改用 WebAssembly 重写后,体积仅有 90k 左右。虽然使用 WebAssembly 需要引入一个 50k-100k 的 JavaScript 类库作为基础设施,但是总体来看资源尺寸的优势还是很大的。

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

在运行 benchmark 等极限测试时,游戏引擎使用 WebAssembly 并不比 JavaScript 有几何量级的提升。笔者的推论是:由于 JavaScript 引擎的 JIT 机制会把经常运行的函数进行极限的编译优化,所以在 benchmark 这种代码大量反复执行的测试环境下,无论是 JavaScript 版本,还是 WebAssembly 版本,运行的都是高度优化后的机器码,虽然 WebAssembly 版本仍然比 JavaScript 版有一定的性能优势,但是并不明显。

在运行业务逻辑代码时,由于大部分业务逻辑代码只运行一次,所以 JavaScript 引擎只会对这部分代码进行简单的编译优化而非极限优化,所以运行这一部分代码 WebAssembly 相比 JavaScript 版本而言提升巨大,但是因为上文所述,不建议开发者在编写业务逻辑时使用 WebAssembly,所以这里陷入了一个两难。在目前而言,理想情况是除了底层库之外,部分关键的涉及性能问题的逻辑也可以使用 WebAssembly 进行编写。

在目前阶段,WebAssembly 适合大量密集计算、并且无需频繁与 JavaScript 及 DOM 进行数据通讯的场景。比如游戏渲染引擎、物理引擎、图像音频视频处理编辑、加密算法等。

Reference

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade