在浏览器之争中,Chrome凭借JavaScript的卓越性能取得了市场主导地位,然而由于javascript的无类型特性,导致其运行时消耗大量的性能做为代价,这也是JavaScript的瓶颈之一。WebAssembly旨在解决这一问题。本文从WebAssembly的起源到开发实践对其做全面探究,帮助开发者对WebAssembly有全面的了解。
缘起
让我们从浏览器大战说起。微软凭借Windows系统捆绑Internet Explorer的先天优势击溃Netscape后,进入了长达数年的静默期。而Netscape则于1998年将Communicator开源,并由Mozilla基金会衍生出Firefox浏览器,在2004年发布了1.0版本。从此,第二次浏览器大战拉开帷幕。这场大战由Firefox浏览器领衔,Safari、Opera等浏览器也积极进取,Internet Explorer的主导地位首次受到挑战。2008年Google推出Chrome浏览器,不但逐步侵蚀Firefox的市场,更是压制了老迈的Internet Explorer。在此次大战之后的2012年,StatCounter的数据指出Chrome以微弱优势超越Internet Explorer成为世界上最流行的浏览器。
分析Google Chrome浏览器战胜Internet Explorer的原因,除了对Web标准更友善的支持外,卓越的性能是其中相当重要的因素,而浏览器性能之争的本质则体现在JavaScript引擎。此前,JavaScript引擎的实现方式经历了遍历语法树到字节码解释器等较为原始的方式,将每条源代码翻译成相应的机器码并执行,并不保存翻译后的机器码,使得解释执行很慢。2008年9月,Google发布了V8 JavaScript引擎。V8被设计用于提高Web浏览器中JavaScript的执行性能,通过即时编译JIT(Just-In-Time)技术,在执行时将JavaScript代码编译成更为高效的机器代码并保存,下次执行同一代码段时无需再编译,使得JavaScript获得了几十倍的性能提升。
然而,JavaScript是个无类型(untyped,变量没有类型)的语言,这直接导致表达式c=a+b有多重含义:
- a、b均为数字,则算术运算符+表示值相加;
- a、b为字符串,则+运算符表示字符串连接;
- …
表达式执行时,JIT编译器需要检查a和b的类型,确定操作行为。若a、b均为数字,JIT编译器则将a、b确认为整型,而一旦某一变量变成字符串,JIT编译器则不得不将之前编译的机器码推倒重来。由此可见,JavaScript的无类型特性建立在消耗大量性能代价的基础之上。即便JIT编译器在对变量类型发生变化时已进行相应优化,但仍然有很多情况JavaScript引擎未进行或无法优化,例如for-of、try-catch、try-finally、with语句以及复合let、const赋值的函数等。
由此可见,JavaScript的无类型是JavaScript引擎的性能瓶颈之一,改进方案有两种:一是设计一门新的强类型语言并强制开发者进行类型指定;二是给现有的JavaScript加上变量类型。
微软开发的TypeScript属于第一种改进方案。它是扩展了JavaScript特性的语言,包含了类型批注,编译时类型检查,类型推断和擦除等功能,TypeScript开发者在声明变量时指定类型,使得JavaScript引擎能够更快将这种强类型的语言编译成弱类型。
看看第二种方案:
代码1表示带有两个参数(a和b)的JavaScript函数,和通常JavaScript代码不同的地方在于a=a | 0及b=b | 0,以及返回值后面均利用标注进行了按位OR操作。这么做的优点是使JavaScript引擎强制转换变量的值为整型执行。通过标注加上变量类型,JavaScript引擎就能更快地编译。
既然增加变量类型能够提升Web性能,有没有办法将静态类型代码例如C/C++等转换成JavaScript指令的子集呢?上面的这段代码恰恰是作为JavaScript子集的asm.js,由代码2的C语言编译而来:
事实上,早在1995年起就已经有Netscape Plugin API(NPAPI)在内的可以使用浏览器运行C/C++程序的项目在开发。而2013年问世的asm.js是目前较为广泛的方案。asm.js是一种中间编程语言,允许用C/C++语言编写的计算机软件作为Web应用程序运行,并保持更好的性能,而Mozilla Firefox从版本22起成为第一个为asm.js特别优化的网页浏览器。
Google也同样在为原生代码运行在Web端而努力。Google Native Client(NaCl)采用沙盒技术,让Intel x86、ARM或MIPS子集的机器码直接在沙盒上运行。它能够在无需安装插件的情况下从浏览器直接运行原生可执行代码,使Web应用程序可以用接近于机器码运作的速度来运行。而Google Portable Native Client(PNaCl)则稍有变化,通过一些前端编译器将C/C++源代码编译成LLVM的中间字节码而不是x86或ARM代码,并且进行优化以及链接(如表1所示)。
有了类型支持,第二种方案性能提升潜力远远大于第一种。
然而,无论是asm.js或现有PNaCl的解决方案,都面临着一些缺陷(例如1KB的C源码编译生成asm.js后的大小有480KB)或其他浏览器不支持的窘境,而2016年10月对Chromium问题跟踪代码的评论更是表明,Google Native Client小组已被关闭。
作为Web浏览器性能和代码重用的解决方案,asm.js及PNaCl都没能被普遍接受,那么有没有上述表格中的特性全部占优,且跨厂商的解决方案呢?
WebAssembly旨在解决这个问题。
新时代
WebAssembly(简称Wasm)是一种新的适合于编译到Web的,可移植的,大小和加载时间高效的格式。这是一个新的与平台无关的二进制代码格式,目标是解决JavaScript性能问题。这个新的二进制格式远小于JavaScript,可由浏览器的JavaScript引擎直接加载和执行,这样可节省从JavaScript到字节码,从字节码到执行前的机器码所花费的即时编译JIT(Just-In-Time)时间。 作为一种低级语言,它定义了一个抽象语法树(Abstract Syntax Tree,AST),开发人员可以以文本格式进行调试。
WebAssembly描述了一个内存安全的沙箱执行环境,可以在现有的JavaScript虚拟机中实现。 当嵌入到Web中时,WebAssembly将强制执行浏览器的同源和权限安全策略。因此,和经常出现安全漏洞的Flash插件相比,WebAssembly是一个更加安全的解决方案。
WebAssembly可由C/C++等语言编译而来。此外,WebAssembly由Google、Mozilla、微软以及苹果公司牵头的W3C社区组共同努力,基本覆盖主流的浏览器厂商,因此其可移植性相较Silverlight等有极大提升,平台兼容问题将不复出现。
在Web平台的很多项目中,对于原生新功能的支持需要Web浏览器或Runtime提供复杂的标准化的API来实现,但是JavaScript API往往较慢。使用WebAssembly,这些标准API可以更简单,并且操作在更低的水平。例如,对于一个面部识别的Web项目,对于访问数据流我们可以由简单的JavaScript API实现,而把面部识别原生SDK做的事情交由WebAssembly实现。
需要了解的是,WebAssembly不是将C/C++等其他语言编译到JavaScript,更不是一种新的编程语言。
探究
asm.js
上文的c语言求和代码经由编译器生成asm.js后如代码3所示。
上述代码转换为WebAssembly的文本格式稍显复杂,为了理解方便,我们从精简的asm.js开始(见代码4)。
wast文本文件
将asm.js代码转换为WebAssembly的文本格式 add.wast(转换工具见本文工具链章节,如代码5所示)。
WebAssembly中代码的可装载和可执行单元被称为一个模块(module)。在运行时,一个模块可以被一组import值实例化,多个模块实例能够访问相同的共享状态。目前文本格式中的module主要用S表达式来表示。虽然S表达格式不是正式的文本格式,但它易于表示AST。WebAssembly也被设计为与ES6的modules集成。
一个单一的逻辑函数定义包含两个部分:功能部分声明在模块中每个内部函数定义的签名,代码段部分包含由功能部分声明的每个函数的函数体。WebAssembly是带有返回值的静态类型,并且所有参数都含有类型。上面的add.wast可以解读为:
- 声明了一个名为$add的函数;
- 包含两个参数a和b,两者都是32位整型;
- 结果是一个32位整型;
- 函数体是一个32位的加法:
- 上面是局部变量$a得到的值;
- 下面是局部变量$b得到的值;
- 由于没有明确的返回节点,因此return是该加法函数的最后加载指令。
二进制Wasm文件
如图1所示,由C语言求和代码经过编译生成二进制文件,通读文件可以找到相应的头部、类型、导入、函数以及代码段等。通过JavaScript API载入Wasm二进制文件后,最终转换到机器码执行。
工具链
开发人员现在可以使用相应的工具链从C / C ++源文件编译WebAssembly模块。WebAssembly由许多工具支持,以帮助开发人员构建和处理源文件和生成的二进制内容。
Emscripten
Emscripten是其中无法回避的工具之一,如图2所示。在图2中,Emscripten SDK管理器(emsdk)用于管理多个SDK和工具,并且指定当前正被使用到编译代码的特定SDK和工具集。
Emscripten的主要工具是Emscripten编译器前端(emcc),它是例如GCC的标准编译器的简易替代实现。
Emcc使用Clang将C/C++文件转换为LLVM(源自于底层虚拟机Low Level Virtual Machine)字节码,使用Fastcomp(Emscripten的编译器核心,一个LLVM后端)把字节码编译成JavaScript。输出的JavaScript可以由Node.js执行,或者嵌入HTML在浏览器中运行。这带来的直接结果就是,C和C++程序经过编译后可在JavaScript上运行,无需任何插件。
WABT和Binaryen
除此之外,对于想要使用由其他工具(如Emscripten)生成的WebAssembly二进制文件感兴趣的开发者,目前http://webassembly.org/官方额外提供了另外两组不同的工具:
- WABT ——WebAssembly二进制工具包;
- Binaryen——编译器和工具链。
WABT工具包支持将二进制WebAssembly格式转换为可读的文本格式。其中wasm2wast命令行工具可以将WebAssembly二进制文件转换为可读的S表达式文本文件。而wast2wasm命令行工具则执行完全相反的过程。
Binaryen则是一套更为全面的工具链,是用C++编写成用于WebAssembly的编译器和工具链基础结构库(如图3所示)。WebAssembly是二进制格式(Binary Format)并且和Emscripten集成,因此该工具以Binary和Emscript-en的末尾合并命名为Binaryen。它旨在使编译WebAssembly容易、快速、有效。它包含且不仅仅包含下面的几个工具。
- wasm-as:将WebAssembly由文本格式(当前为S表达式格式)编译成二进制格式;
- wasm-dis:将二进制格式的WebAssembly反编译成文本格式;
- asm2wasm:将asm.js编译到WebAssembly文本格式,使用Emscripten的asm优化器;
- s2wasm:在LLVM中开发,由新WebAssembly后端产生的.s格式的编译器;
- wasm.js:包含编译为JavaScript的Binaryen组件,包括解释器、asm2wasm、S表达式解析器等。
Binaryen目前提供了两个生成WebAssembly的流程,由于emscripten的asm.js生成已经非常稳定,并且asm2wasm是一个相当简单的过程,所以这种将C/C ++编译为WebAssembly的方法已经可用(如图4所示)。
由此可见,Emscripten以及Binaryen提供了完整的C/C++到WebAssembly的解决方案。而Binaryen则帮助提升了WebAssembly的工具链生态。
提示
由于WebAssembly正处于活跃开发阶段,各项编译步骤和编译工具会有大幅变更和改进,相信最终的编译工具和步骤会趋于便捷,开发者需要留意官方网站的最新动态。
实战
Linux和mac OS平台编译原生代码到WebAssembly可由如下步骤实现。
编译环境准备
操作系统必须有可以工作的编译器工具链,因此需要安装GCC、cmake环境,此外Python、node.js及Java环境也是需要的(其中Java为可选,如图5所示)。
如果是以其他方式安装了Node.js,可能需要更新~/.emscripten文件的NODE_JS属性。
安装正确的emscripten分支
要编译原生代码到WebAssembly,我们需要emscripten的incoming分支。由于emscripten不仅仅是用于WebAssembly的编译工具链,选择正确的分支尤为重要(如图6所示)。
其中URLTO具体的URL是https://s3.amazonaws.com/mozilla-games/emscripten/releases/emsdk-portable.tar.gz。
处理安装异常
可运行emcc -v命令进行验证安装。如果遇到如图7所示的错误,表明带有JavaScript后端的LLVM编译器并未被生成。
通过图8步骤,可以解决该问题,并且在~/.emscripten 文件中修改如下配置:
开始编译程序
现在一个完整的工具链已经具备,我们可以使用它来编译简单的程序到WebAssembly。但是,还有一些其他注意事项:
- 必须通过参数-s Wasm=1到emcc(否则默认emcc将编译出asm.js);
- 除了Wasm二进制文件和JavaScript wrapper外,如果还希望emscripten生成一个可直接运行的程序的HTML页面,则必须指定一个扩展名为.html的输出文件。
在编译之前,首先准备一个最基本的add.c程序,见代码6。
按代码7所示的命令编辑好add.c程序并编译:
运行WebAssembly应用
以Chrome浏览器为例,如果直接在浏览器内本地打开HTML文件,会有图9所示的错误:
由于XMLHttpRequest跨域请求不支持file://协议,必须经由HTTP实际输出,可以由python的SimplHTTPServer改进,见代码8:
在浏览器中输入http://127.0.0.1:8080并打开add.html,就能直接看到转换成WebAssembly的应用程序输出结果。
创建独立WebAssembly
默认情况下,emcc会创建JavaScript文件和WebAssembly的组合,其中JS加载包含编译代码的WebAssembly。对于C/C++开发人员,他们可能更倾向于创建独立的WebAssembly,用于JavaScript开发人员调用,见代码9。
上述命令运行后,我们可以得到独立的Wasm文件。需要说明的是,该参数仍然在开发中,可能随时发生规范和实现变更。
JavaScript API调用
从C/C++程序编译获得一个.wasm模块之后,JavaScript开发人员可以通过如下方式进行载入.wasm文件并执行。WebAssembly社区组也有计划通过Streams使用streaming以及异步编译,见代码10。
最后一行调用导出的WebAssembly函数,它反过来调用我们导入的JS函数,最终执行add(201700, 2),并且在控制台获得期望的结果输出(如图10所示)。
性能
那么,WebAssembly的真实性能如何呢?首先我们用一直被用来作为CPU基准测试的斐波那契 (Fibonacci)数列来进行对比,这里使用的是性能较差的递归算法,在Node.js v7.2.1环境下,能够看到WebAssembly性能优势越发明显(如图11所示)。
再看看最基本的1000毫秒时间内,求和计算的运算量统计,在同一台计算机的Firefox 50.1.0版本的运算结果如图12所示。
尽管重复测试时结果不尽相同,重启浏览器并多次测试取平均值后依然可以看到WebAssembly的运算量比JavaScript快了近一个量级。
Demo
图13展示了Angry Bots Demo,它是由WebAssembly项目发布的一个Demo,由Unity游戏移植而来。
通过如下方式可以体验WebAssembly在浏览器中的强大性能。即便Google Chrome较新的稳定版也已支持WebAssembly,还是推荐使用canary版及Firefox的nightly版进行测试。
- 下载浏览器:
1-1. Google Chrome;
1-2. Mozilla Firefox;
1-3. Opera;
1-4. Vivaldi。 - 打开 WebAssembly支持 :
2-1. Google Chrome:chrome://flags/#enable-webassembly;
2-2. Mozilla Firefox:about:config→接受→搜索javascript.options.wasm→设置为true;
2-3. Opera:opera://flags/#enable-webassembly;
2-4. Vivaldi:vivaldi://flags#enable-webassembly。
访问:http://webassembly.org/demo/。
使用W、A、S、D等键实现移动操作,点击鼠标进行射击。该WebAssembly游戏在浏览器中运行相当流畅,媲美原生性能。
除了最新的浏览器开始对WebAssembly逐步支持外,Intel开源技术中心开发的Crosswalk项目(https://crosswalk-project.org/)早在2016年11月初的Crosswalk 22稳定版(Windows及Android 平台)即已加入对WebAssembly实验性的支持,开发者可以使用该版本体验Angry Bots Demo。
开发者
WebAssembly对于Web有显著的性能提升,对于开发者尤其是前端或者JavaScript开发人员而言,并不意味着WebAssembly将会取代JavaScript(如图14所示)。
WebAssembly被设计为对JavaScript的补充,而不是替代,是为了提供一种方法来获得应用程序的关键部分接近原生性能。随着时间的推移,虽然WebAssembly将允许多种语言(不仅仅是C/C++)被编译到Web,但是JavaScript的发展势头不会因此被削弱,并且仍然将保持Web的单一动态语言。此外,由于WebAssembly构建在JavaScript引擎的基础架构上,JavaScript和WebAssembly将在许多场景中配合使用。
那么WebAssembly是不是仅仅面向C/C++开发者呢?答案依旧是否定的。WebAssembly最初实现的重点是C/C++,由Mozilla主导开发的注重高效、安全和并行的Rust也能在2016年末被成功编译到WebAssembly了,未来还会继续增加其他语言的支持,见代码11。
在未来,通过ES6模块接口与JavaScript集成,Web开发人员并不需要编写C++,而是可以直接利用其他人编写的库,重用模块化C++库可以像使用JavaScript中的modules一样简单。
进展
依据开发路线图,2016年10月31日,WebAssembly到达浏览器预览的里程碑。Google Chrome V8引擎及Mozilla Firefox SpiderMonkey引擎都已经在trunk上支持WebAssembly浏览器预览。2016年12月下旬,Microsoft Edge浏览器使用的JavaScript引擎ChakraCore v1.4.0启用了WebAssembly浏览器预览支持。而Webkit JavaScriptCore引擎对于该支持也在积极进行中。
目前,WebAssembly社区组已经有初始(MVP)二进制格式发布候选和JavaScript API在多个浏览器中实现。作为浏览器预览期间的一部分,WebAssembly社区组(WebAssembly Community Group)现在正在征求更广泛的社区反馈。社区组的初步目标是浏览器预览在2017年第一季度结束,但在浏览器预览期间的重大发现可能会延长该周期。当浏览器预览结束时,社区组将产生WebAssembly的草案规范,并且浏览器厂商可以开始默认提供符合规范的实现。预计在2017年上半年,四大主流浏览器对原生的WebAssembly支持将到达稳定版。
具体到Google V8引擎的最新进展,asm.js代码将不再通过Turbofan JavaScript编译器而是编译到WebAssembly后,在WebAssembly的原生执行环境中执行最终的机器码。这种改变带来的好处有,为asm.js将预先编译(AOT,Ahead Of Time Compilation)带到了Chrome,且完全向后兼容。新的WebAssembly编译渠道重用了一些Turbofan JavaScript编译器后端部分,因此能够在少了很多编译和优化消耗的前提下,产生类似的代码。在Google Chrome中,WebAssembly将很快在Canary版中默认启用,开发团队也期望能够发布到2017年第一季度末的稳定版中。
社区
包含所有主要浏览器厂商代表的W3C Web——Assembly社区组于2015年4月底成立。该小组的任务是,在编译到适用于Web的新的、便携的、大小和加载时间高效的格式上,促进早期的跨浏览器协作。该社区组也正在将WebAssembly设计为W3C开放标准。目前,除了文中所述主流浏览器厂商Mozilla、Google、微软、及苹果公司之外,Opera CTO及Intel的8位该领域专家均参与了该社区组。当然,并不是只有社区组成员才能参与标准的制定,任何人都可以在https://github.com/WebAssembly做出贡献。
展望
由于主要的浏览器厂商对WebAssembly支持表现积极,并且都在实现WebAssembly的各项功能,因此在Web中高性能需求的应用例如在线游戏、音乐、视频流、AR/VR、平台模拟、虚拟机、远程桌面、压缩及加密等都能够获得接近于原生的性能。相信WebAssembly将会开创Web的新时代。
原文: