无疑是近年来让人最为兴奋的新技术之一,它虽始于浏览器但已经开始不断地被各个语言及平台所集成。在实际的工业化落地中,区块链、边缘计算、游戏及图像视频等多个领域都依靠 webassembly 创造了让人称赞的产品。webassembly 技术本身具有非常多优点,其中最为被人所熟知的三点有:
二进制格式
low-level的编译目标
接近native的执行效率
那么 webassembly 是从何演变而来,它为什么具有这些优点与特性,又是如何被标准化的,更重要的是作为普通开发者,我们应如何更好地入手 webassembly 开发及实践呢?本专题将围绕 webassembly 及工具链,通过一系列文章依次介绍 webassembly 的演变历程、工具链使用、实践案例、最新应用场景及使用技巧,帮助普通开发者正确理解 webassembly 的使用场景,并能够顺利使用 emscripten 工具链完成自己的 webassembly 相关项目。
本文作为专题的第一篇文章,将会较为详细地介绍 webassembly 的演变历程,使读者深入理解 webassembly 这门技术的使用场景,从而更好地学习和使用 webassembly 技术。
javascript 的弊端
毫无疑问是技术领域的佼佼者。自 brendan eich 于 1995 年花费 10 天时间为 netscape 开发出 javascript 为始,到现在已经走过了 20 多个年头。随着技术的蓬勃发展,不管是 npm 与 github 上丰富的 javascript 库与框架,还是、node.js、electron、quickjs 等领域技术的出现,无一不彰显着 javascript 生态的繁荣,javascript 这门语言也变得越来越流行和重要。
但与此同时,随着各类应用功能的复杂化,受限于 javascript 语言本身动态类型和解释执行的设计,其性能问题也逐渐凸现。我们急需新技术帮助我们解决 javascript 的性能问题。在 2008 年底,google、apple、mozilla 为 javascript 引入了 jit(just-in-time)引擎,试图解决 javascript 的性能问题,并取得了非常好的效果。其中的佼佼者非 google 的 v8 莫属,其大举提升了 javascript 的性能,并拉开了 javascript 引擎竞速的序幕。
那 jit(just-in-time)引擎是如何提升 javascript 性能的呢?
我们知道,由于 javascript 是解释型语言,因此 javascript 引擎需要逐行将 javascript 代码翻译为可执行的代码。可执行代码有多种形式,其中较为常见的是基于 ast 的直接执行以及 bytecode 的执行方式。显而易见,这些做法相比于直接运行机器码而言都并不高效,如果我们能根据代码的执行频次将部分代码实时编译为机器码,就能获得更大的性能提升。这就是 jit(just-in-time)的基本思路。
在实际生产中,jit(just-in-time)引擎一般会引入多层次的决策来优化代码:
warm阶段(解释执行的代码被执行多次): 将解释执行的代码发送给jit(just-in-time)引擎,并创建出编译为机器码的执行代码,但此处并不进行替换;
hot阶段(解释执行的代码被执行得十分频繁): 解释执行代码被替换为warm阶段的机器码执行代码;
very hot阶段:将解释执行的代码发送给优化编译器(optimising compiler),创建和编译出更高效的机器码的执行代码并进行替换;
假设我们的 javascript 代码中有部分代码被执行了多次,此时这部分代码会被标记为 warm,同时被送往 jit(just-in-time)引擎进行优化。jit(just-in-time)引擎此时会针对这些代码逐行进行机器码编译,然后存储在一张表的单元中(实际上表单元仅指向了被编译的机器码)。当解释执行的代码被执行得非常频繁时会进入 hot 阶段,jit(just-in-time)引擎会将解释执行的代码直接替换为编译的机器码版本。
需要注意的是,表单元的引用依据实际上会依赖于行号以及参数类型,假设我们有如下的代码:
由于数组 arr 中存在两种数据类型(number/string),当我们多次执行相关代码时,dosomething
函数会被 jit(just-in-time)引擎创建并编译出两个不同类型的机器码执行代码版本,并且使用不同的表单元引用。当然,由于机器码执行代码的创建和编译存在代价,因此不同的 jit(just-in-time)引擎会有不同的优化策略。
如果部分代码执行得异常频繁,那么自然的这部分解释执行的代码会被发送给优化编译器(optimising compiler)进行更高程度的优化,从而创建并编译出相比 warm 阶段更高效的机器码执行代码版本。
与此同时,在创建这些高度优化的机器码执行代码期间,编译器将会严格限制执行代码的适用类型(比如仅适用于 number/string 或某些特定类型参数),并且在每次调用执行前都会检查参数类型。如果匹配则使用这些高度优化的机器码执行代码,否则将会回退到 warm 阶段生成的机器码执行代码或是直接解释执行。
javascript 有了 jit(just-in-time)后就能高枕无忧了么?不尽然。从上面的介绍中我们可以看到,jit(just-in-time)引擎的优化并非是完全无代价的。同时由于 javascript 自身的灵活性,如果我们编写 javascript 代码时并没有将数据类型严格固定,那么 jit(just-in-time)的效果将会大打折扣。在 google v8 团队的文章中我们可以看到,使用 jit-less 模式的 v8 在运行 youtube 的 living room 页面时,其测试成绩与使用 jit 的 v8 实际差距仅为 6%。这个测试侧面反应了 jit 在生产中并不是完全的“性能银弹”。
jit-less 模式下 v8 与基线的对比
那么 javascript 能变得更快吗?还是说我们需要其他技术来解决 javascript 的性能问题?此时 nacl 和 pnacl 应运而生。
nacl 与 pnacl
尽管 javascript 由于 jit 的加入在性能上有了很大的提升,但在许多性能敏感的领域,javascript 仍旧无法满足需求。因此在 2008 年,google 的 brad chen、bennet yee 以及 david sehr 开源了 nacl 技术,2009 年,nacl 技术正式达到生产可用状态。nacl 全称为“native client”,其由 c/c 语言编写并定义了一套 native code 的安全子集(sfi 技术),同时执行于自己独立的沙盒环境之中,以防止安全性未知的 c/c 代码对操作系统本身产生危害。
nacl 应用及其模块在性能上与原生应用的差距非常小,但由于 nacl 与 cpu 架构强关联且不具有可移植性,需要针对不同的平台进行开发和编译,导致开发者无法自由分发 nacl 应用及模块。为了解决这个问题,nacl 改进技术 pnacl 出现了。
nacl 的性能损耗极小
pnacl 的全称为"portable native client",其通过替换 native code 为 llvm ir 子集并在客户端编译为 nacl 的方式解决了 nacl 的分发问题。pnacl 不依赖于特定的 cpu 架构,更易于被部署和使用,“一次编译,到处运行”在 pnacl 上得到了实现。但同样的,pnacl 也是运行在自己的独立沙盒之中,其无法直接的访问 web apis,而是需要通过一个名为“ppapi”的接口来与 javascript 通信。
pnacl 技术在当时看起来是一个非常理想的方案,其兼具高性能和易于分发的特点,但实际上在当时并没有受到非常强的支持。ppapi 出现的时代正好是处于人们尽可能试图摆脱 flash、java applet 等插件的时代,尽管当时 chrome 已经直接集成了 nacl 与 pnacl,但其运行在独立沙盒环境与使用独立 api 的方式,跟 flash、java applet 等插件非常类似。同时,其开发难度、成本以及糟糕的兼容性问题(2011 年开始 firefox 及 opera 正式支持 ppapi 及 nacl)都成为了 nacl/pnacl 普及的最大障碍。
让人惊艳的 asm.js
谈到 asm.js 和 webassembly,就不得不提其中的关键人物。2010 年,alon zakai 结束了两年的创业项目,加入 mozilla 负责 android 版 firefox 的开发。在 mozilla 的本职工作之外,alon zakai 继续编写着自己的 c/c 游戏引擎。在项目临近尾声之时,alon zakai 突发奇想,想将自己的 c/c 游戏引擎运行在浏览器上。在 2010 年,nacl 还是一门非常新的技术,而 pnacl 才刚刚开始开发,此时并没有一个非常好的技术方案能够将 alon 的 c/c 游戏引擎跑在浏览器上。但好在 c/c 是强类型语言,而 javascript 是弱类型语言,将 c/c 代码编译为 javascript 代码在技术实现上是完全可行的。于是 alon zakai 自此开始编写相关的 compiler 实现,emscripten(llvm into javascript)
由此诞生了!
到 2011 年,emscripten 已经具备编译像 python 以及 doom 等中大型项目的能力,与此同时 emscripten 也在 jsconfeu 会议上首次亮相,并取得了一定的影响力。看到了 emscripten 项目的巨大潜力(相较于 nacl 而言对 web 更加友好),brendan 及 andreas 邀请 alon 加入 mozilla 的 research 团队全职负责 emscripten 项目的开发,alon zakai 欣然接受并将工作的重心放在了如何提升 emscripten 编译的 javascript 代码执行速度上。
在javascript的弊端
章节中我们可以看到,尽管 javascript 拥有 jit(just-in-time),但由于 javascript 本身的语言特性,导致 jit(just-in-time)难以被预测,在实际的生产环境当中 jit(just-in-time)的效果往往并没有那么显著。
为了使得 javascript 运行得更快,我们应该要更充分地利用 jit(just-in-time),因此在 2013 年,alon zakai 联合 luke wagner、david herman 发布了 asm.js。
asm.js 的思想很简单,就是尽可能明确对应的类型,以便 jit(just-in-time)被充分利用。如下图示例所示:
我们可以看到,对于add
函数而言,由于传入参数x
、y
以及返回值进行了|0
的操作,其能够很明确地为 jit(just-in-time)指明对应的类型(i32),因此可以被 jit(just-in-time)充分优化(不考虑后期 aot 的情况)。
通过添加类似的类型注解,emscripten 编译的 asm.js 在运行速度上相比普通 javascript 有了质的飞跃。在 benchmark 中,asm.js 能达到 native 性能的 50%左右,相比于普通的 javascript 代码而言取得了极大的性能提升,这无疑是让人兴奋的成果。但是 asm.js 自身也存在一些无法忽视的问题,其总体而言并不是一个非常理想的技术方案。
最显而易见的就是 asm.js 代码的“慢启动”问题。由于 asm.js 还是和 javascript 一样的文本格式,因此对于大中型项目而言,其解析花费的时间会非常长,无法与高效的二进制格式相提并论。
其次,asm.js 实质上是一种较为 hack 的实现方式,类似|0
的类型标注不具有可读性,同时拓展 asm.js 也变得越来越复杂且不可靠:随着 asm.js 想要更加接近于 native 的执行性能,不免会对诸多 math 函数(例如 math.imul 及 math.fround 等)进行拓展和改写。从长远来看,这对 tc39 标准的制定并不友好,同时 asm.js 自身的相关实现(例如 memory growth 等)也遭遇了非常多的问题,导致 asm.js 标准被迫不断修订。“the hacks had a cost”,我们需要一个全新的技术来解决 asm.js 所遇到的这些问题。
合作共赢 - webassembly
在 2013 年,nacl/pnacl 与 asm.js/emscripten 形成了不同路线发展的竞争态势,但与此同时,google 及 mozilla 也在工具及虚拟机层面加强了许多合作,其中包括:
由google的jf bastien牵头,每月google和mozilla工具团队之间开展交流会;
emscripten和pnacl开始共享部分代码,包括legalization passes、le32 triple等;
尝试将nacl应用通过emscripten编译,并开源pepper.js;
google及mozilla共同向asm.js贡献代码,并规划未来native code在web上的合理方案;
就webassembly前身“webasm”进行标准和方案的讨论;
最终在 2015 年的 4 月 1 号,“webassembly”击败了“webasm”、“webmachine”和其它名称,在 google 和 mozilla 的团队交流邮件中被确定使用。至 2015 年 6 月 17 号,两方就 webassembly 的标准化工作达成一致,并搭建了 webassembly 金马国际官网开始对外宣传。webassembly 的设计汲取了 nacl 与 asm.js 两者的优点:
webassembly并不依赖于javascript,与nacl/pnacl一样,它基于二进制格式,能够被快速解析;
与asm.js一样,依靠emscripten等工具链提供的api,它以非常自然的方式直接操作web apis,而不用像pnacl一样需要处理与javascript之间的通信;
webassembly依赖于llvm ir并使用独立的vm环境,因此其它语言/平台能够以较低成本接入,同时能够且易于被持续优化至接近native的性能;
目前各大主流浏览器已经完全实现了 webassembly 的 mvp 版本,并将其接纳为“浏览器的第二语言”。依靠优秀的设计,webassembly 也从浏览器平台走向更多平台,wasi(webassembly system interface)将为 webassembly 提供更多的可能性。随着 webassembly 相关标准逐渐确定和完善,webassembly 技术的应用领域将会越来越广。
小结
本文从 javascript 开始,介绍了 nacl/pnacl 以及 asm.js 技术方案的优缺点。通过简单回顾 webassembly 相关历史背景,我们能更好地够理解 webassembly 技术的演变过程及其适用场景。在后面的文章中,我们将基于 emscripten 工具链继续探讨 webassembly,并通过具体的实例介绍 webassembly 应用的基本方法和相关实现。
致谢
感谢 emscripten 核心作者 alon zakai 在我写作此篇文章时对邮件所提问题的耐心解答和帮助,如此才使得我能够更全面、详细及正确地还原有关 webassembly 的技术演变历程。
作者介绍:
,coupang 资深全栈工程师,曾任百度、腾讯、全民直播前端工程师,modern web/gmtc/fdcon 等多个会议讲师,编写了多个 webassembly 项目实践,目前正在尝试 webassembly 在图形相关领域的实践。
评论