WebAssembly如何演进成为“浏览器第二编程语言”?

浅谈WebAssembly演进史

2020 年 8 月 13 日

WebAssembly如何演进成为“浏览器第二编程语言”?

WebAssembly无疑是近年来让人最为兴奋的新技术之一,它虽始于浏览器但已经开始不断地被各个语言及平台所集成。在实际的工业化落地中,区块链、边缘计算、游戏及图像视频等多个领域都依靠 WebAssembly 创造了让人称赞的产品。WebAssembly 技术本身具有非常多优点,其中最为被人所熟知的三点有:


  • 二进制格式

  • Low-Level的编译目标

  • 接近Native的执行效率


那么 WebAssembly 是从何演变而来,它为什么具有这些优点与特性,又是如何被标准化的,更重要的是作为普通开发者,我们应如何更好地入手 WebAssembly 开发及实践呢?本专题将围绕 WebAssembly 及Emscripten工具链,通过一系列文章依次介绍 WebAssembly 的演变历程、工具链使用、实践案例、最新应用场景及使用技巧,帮助普通开发者正确理解 WebAssembly 的使用场景,并能够顺利使用 Emscripten 工具链完成自己的 WebAssembly 相关项目。


本文作为专题的第一篇文章,将会较为详细地介绍 WebAssembly 的演变历程,使读者深入理解 WebAssembly 这门技术的使用场景,从而更好地学习和使用 WebAssembly 技术。


JavaScript 的弊端


JavaScript毫无疑问是技术领域的佼佼者。自 Brendan Eich 于 1995 年花费 10 天时间为 Netscape 开发出 JavaScript 为始,到现在已经走过了 20 多个年头。随着技术的蓬勃发展,不管是 NPM 与 GitHub 上丰富的 JavaScript 库与框架,还是React Native、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)引擎会将解释执行的代码直接替换为编译的机器码版本。


需要注意的是,表单元的引用依据实际上会依赖于行号以及参数类型,假设我们有如下的代码:


function doSomething(value){    // performing some operations}
const arr = [0, "String"];for (let i = 0; i < arr.length; i++) { doSomething(arr[i])}
复制代码


由于数组 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》文章中我们可以看到,使用 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,就不得不提其中的关键人物Alon Zakai。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 会议上首次亮相,并取得了一定的影响力。Mozilla看到了 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函数而言,由于传入参数xy以及返回值进行了|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 在图形相关领域的实践。


2020 年 8 月 13 日 08:002359

评论

发布
暂无评论
发现更多内容

第三周总结

Geek_ac4080

这可能是GitHub上最适合计算机专业学生看的编程教程

小Q

Java 学习 编程 面试 基础

MySQL-技术专题-主从复制原理

李浩宇/Alex

节日快乐…吗?

小天同学

个人感悟 国庆中秋 假期 节日

架构师训练营1期第三周作业

木头发芽

字节跳动 Flink 单点恢复功能实践

Apache Flink

flink

私有云PAAS平台的思考

8小时

如果朋友圈没有点赞功能,你还会发朋友圈吗

彭宏豪95

微信 产品 互联网 写作

基于区块链技术实现“资产通证化”

CECBC区块链专委会

资产证券化 流动性

~~寒露节记~~

wo是一棵草

JavaScript 语言通识 — 重学 JavaScript

三钻

Java 前端进阶

usdt承兑商支付系统开发源码,区块链支付搭建

WX13823153201

「剑指offer」27道Mybatis面试题含解析

Java架构师迁哥

架构师训练营第三周:系统架构

m

第四周

Geek_fabd84

各角色如何从DevOps中受益?

DevOps 产品经理 测试 开发 运维工程师

架构师训练营第一期 - 第四周课后 - 作业一

极客大学架构师训练营

国庆期间,我造了台计算机

yes的练级攻略

计算机 底层

Python时间序列分析简介(1)

计算机与AI

Python pandas 数据处理 时间序列

手把手教你锤面试官 03——Spring怎么那么简单

慵懒的土拨鼠

Nginx 整合 FastDFS 实现文件服务器

哈喽沃德先生

nginx 文件系统 分布式文件存储 fastdfs 文件服务器

开源的意义与价值

Braisdom

Java 开源 ORM

如何使用 dotTrace 来诊断 netcore 应用的性能问题

newbe36524

微服务 .net core netcore ASP.NET Core

看动画学算法之:linkedList

程序那些事

数据结构和算法 看动画学算法 看动画学数据结构 算法和数据结构

MySQL-技术专题-问题分析

李浩宇/Alex

线上服务平均响应时间太长,怎么排查?

小Q

Java 程序员 测试 Jmeter 性能调优

第三周作业

Geek_ac4080

架构师训练营 第三周作业

haha

极客大学架构师训练营

关于代码审查的一点体会

KJ Meng

敏捷开发 研发管理 代码审查 Code Review

云原生虚机应用托管-设计篇

8小时

区块链技术最重要价值所在

CECBC区块链专委会

区块链 数字经济 经济

WebAssembly如何演进成为“浏览器第二编程语言”?-InfoQ