背景:从 JavaScript 说起
JavaScript 占据着统治地位,不管是公开还是私有的项目、任何组织、世界任何地区,JavaScript 都是第一。 -GitHub 2018 年度报告
随着JavaScript的快速发展,目前它已然成为最流行的编程语言之一,这背后正是 Web 的发展所推动的。但是随着JavaScript被广泛的应用,它也暴露了很多问题:
- 语法太灵活导致开发大型 Web 项目困难;
- 性能不能满足一些场景的需要;
这两大问题成为JavaScript头顶上的达摩克利斯之剑,危及着JavaScript更广泛的应用。
Brendan Eich 做梦也没想到,自己花了十天仓促设计出来的 JavaScript,一经推出就被广泛接受,获得了全世界范围内大量的用户使用。前人挖坑,后人来填。既然JavaScript已经成为了Web编程的事实标准,那么这两个亟待解决的问题势必将要被解决。
MS、Google、Mozilla的探索
MS:TypeScript
第一个问题被著名开源软件大厂MicroSoft解决。
MicroSoft集结了C#的首席架构师以及Delphi和Turbo Pascal的创始人Anders Hejlsberg等明星阵容,打造了TypeScript。
TypeScript它是JavaScript的一个严格超集,并添加了可选的静态类型和使用看起来像基于类的面向对象编程语法操作 Prototype。所以TypeScript可以这样理解:
MicroSoft利用TypeScript这把锋利的武器打造了VSCode等史诗级项目,于是乎,第一把达摩克利斯之剑"语法太灵活导致开发大型 Web 项目困难"似乎已经被解决。
但是,由于TypeScript最终仍然是被编译成JavaScript在浏览器中执行,所以困扰着JavaScript开发者的性能问题,仍然没有被解决。
Google:V8
早在2008年,Google就推出了自家的JavaScript引擎V8,试图使用JIT技术提升JavaScript的执行速度,并且它真的做到了。
由于JIT技术的引入,V8使得Web性能得到了数十倍的增长!
上图展示了Chrome的v8与IE的Chakra benchmark结果。具体的地址点我
既然性能得到了如此大的提升,那么JavaScript广为诟病的性能问题得到了解决吗?为啥Web性能还是被挑战?
单线程 -> 阻塞
Web应用中,性能瓶颈大部分的原因已经不在JavaScript,而在于DOM。浏览器中通常会把DOM和JavaScript独立实现。下图展示了不同浏览器DOM和JavaScript的实现情况:
由于Dom渲染和JavaScript引擎是相对独立的,这两个模块相互访问的时候,都是通过接口访问。由于JavaScript单线程的特性,这种访问只能是单工的。
可以把DOM和JavaScript各自想象为一个岛屿,他们之间用桥梁连接,JavaScript每次访问DOM,都要经过这座桥,并交纳过桥费,访问的次数越多,费用就越高,因此,推荐的做法是尽可能减少过桥的次数,一直待在JavaScript岛上。为了达到这个目的,可以使用Virtual Dom,Web Worker来实现。这里就不再赘述。
JIT VS AOT,在重型计算面前仍然力不从心
刚才谈到,V8引擎首次将JIT技术引入JavaScript当中,大幅提升了执行速度。那么首先我们需要理解什么是JIT,以及AOT。
AOT: Ahead-of-Time compilation
必须是强类型语言,编译在执行之前,编译直接生成CPU能够执行的二进制文件,执行时CPU不需要做任何编译操作,直接执行,性能最佳。
JIT: Just-in-Time compilation
没有编译环节。执行时根据上下文生成二进制汇编代码,灌入CPU执行。JIT执行时,可以根据代码编译进行优化,代码运行时,不需要每次都翻译成二进制汇编代码,V8就是这样优化JavaScript性能的。
由于JavaScript的动态语言类型已无法改变,所以只能采用JIT的形式对性能进行优化。
为了进一步JIT优化效率,继而提升JavaScript性能,浏览器鼻祖Mozilla推出了asm.js。
Mozilla:asm.js
和TypeScript比较相似的是,asm.js同样也是强类型的JavaScript,但是他的语法则是JavaScript的子集,是为了JIT性能优化而专门打造的。
一段典型的asm.js代码如下:
可以看到,asm.js使用了按位或0的操作,来声明x为整形。从而确保JIT在执行过程中尽快生成相应的二进制代码,不用再去根据上下文判断变量类型。
Mozilla给出了asm.js的benchmark:
asm.js To WebAssembly
自从Mozilla提出了asm.js,Google、MicroSoft、Apple都觉得asm.js的思路不错,于是联合起来,一同共建WebAssembly生态。
同asm.js不同的是,WebAssembly是一份字节码标准,以字节码的形式依赖虚拟机在浏览器中运行。
可以依赖Emscripten等编译器将C++/Golang/Rust/Kotlin等强类型语言编译成为WebAssembly字节码(.wasm文件)。所以WebAssembly并不是Assembly(汇编),它只是看起来像汇编而已。一份典型的.wasm文件如下所示:
00000000: 0061 736d 0100 0000 0108 0260 017f 0060 .asm.......`...`
00000010: 0000 0215 0203 656e 7603 6d65 6d02 0001 ......env.mem...
00000020: 026a 7303 6c6f 6700 0003 0201 0107 0b01 .js.log.........
00000030: 0765 7861 6d70 6c65 0001 0a23 0121 0041 .example...#.!.A
00000040: 0042 c8ca b1e3 f68d c8ab ef00 3703 0041 .B..........7..A
00000050: 0841 f2d8 918b 0236 0200 4100 1000 0b .A.....6..A....
复制代码
实战
环境搭建:编译Emscripten
本次使用官方推荐的CPP语言编译成为WebAssembly文件,并在浏览器中执行。首先需要搭建Emscripten环境。Emscripten被用于将CPP文件转换成为WASM字节码文件。
常规的搭建流程十分繁琐:
1、确保安装CMake、Xcode、Python 2.7.x
2、git clone https://github.com/juj/emsdk.git
3、./emsdk install --build=Release sdk-incoming-64bit binaryen-master-64bit
等待约1个小时……,切换版本需要重新编译。
复制代码
不过,早有好心人为我们准备了捷径:使用Docker镜像,快速开启你的WebAssembly之路吧。只需要以下几步:
1、安装Docker
2、docker pull trzeci/emscripten:latest
3、alias emcc='docker run –rm -v $(pwd):/src -u emscripten trzeci/emscripten emcc’
切换版本只需要pull相应的tag
复制代码
编译CPP的MD5函数至WASM
首先需要找到一份计算MD5的CPP代码:
git@github.com:codenoid/md5-cpp.git
使用emscripten.h中的EMSCRIPTEN_KEEPALIVE宏,确保emcc编译器在编译时,不会因为该函数没有被调用而优化掉这个函数。
这里uint8_t* 被隐式类型转换为char*
使用emcc编译CPP文件至WASM文件:
emcc -O3 -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]’ md5.c
复制代码
- -O3: 优化级别,O3是最高优化级别
- -s WASM=1:生成wasm代码,而不是asm.js代码
- -s EXTRA_EXPORTED_RUNTIME_METHODS=‘[“cwrap”]‘:在JavaScript中使用cwrap函数引用导出函数
最后会生成a.out.js和a.out.wasm两个文件。分别是WASM和JavaScript交互的胶水文件以及WebAssembly字节码文件。
计算md5并输出结果:
这里有两点需要注意:
- a.out.js会自动fetch wasm文件,由于获取wasm文件也存在跨域的情况,可以使用http-server本地起一个sever。
- CPP的变量类型以及JavaScript的变量类型需要进行转换,转换由胶水代码自动执行,具体的转换规则如下:
benchmark
既然WebAssembly主打性能提升,那么benchmark就必不可少啦,针对"ivweb"短字符加密100000次,benchmark的结果如下:
可以看到WebAssembly相较于纯JavaScript,计算性能大约提升了39%,这与普遍的100%+的性能提升有着较大差距。这是为什么呢?
我又对2M的长文本进行benchmark对比,结果如下:
这一次的提升就比较大了。是什么造成了如此大的差距呢?我猜测有两点原因:
- 对"ivweb"短字符加密100000次时,JIT优化介入后,不需要每次都去编译,JavasScript性能得到了极大提升。
- 对"ivweb"短字符加密100000次时,胶水代码执行次数较多,拖慢了性能。
针对与以上两点猜测,又做了一组benchmark,加密“ivweb”5000000次
可以看到WebAssembly与纯粹的JavaScript性能差距以及不大了,验证了我的猜想。
本次benchmark代码我已经上传到GitHub仓库中:
git@github.com:PeacefulLion/wasm-benchmark.git
启示
鉴于V8的强大性能,90%的应用场景下你不需要WebAssembly.
启示:如何提高JS代码性能?
- 声明变量时提供默认类型,加快JIT介入
- 不要轻易改变变量的类型
- Node.js像JAVA一样也存在JIT预热?
总结与展望
现在的WebAssembly还并不完美。但是线程的支持,异常处理,垃圾收集,尾调用优化等,都已经加入WebAssembly的计划列表中了。
将来,WebAssembly能够被用于:
- 扩展浏览器端视音频处理能力(H.265)
- 基于WebAssembly的高性能Web应用(加密、游戏、挖矿?)
目前Webpack4已经支持import wasm文件的形式调用wasm文件。
未来,WebAssembly 将可能直接通过 HTML 标签进行引用,比如:
<script src="./wa.wasm"></script>;
复制代码
或者可以通过 JavaScript ES6 模块的方式引用,比如:
import xxx from './wa.wasm';;
复制代码