WebAssembly
从JavaScript性能发展史说起
JavaScript最开始设计出来时就是一门解释型语言,目标用户为“非专业编程人员和设计师”。然而阴差阳错地,由于JavaScript的反响实在是太好了,这门为“非专业编程人员和设计师”设计的解释型语言现在已经成为了互联网上最重要的语言之一。大家想用JavaScript做的东西越来越多,性能问题也自然而然地成为了最受关注的问题之一。
JIT
Google 在 2009 年在 V8 中引入了 JIT 技术 (Just-in-time Compiling 即时编译)。JavaScript瞬间提升了20-40倍的速度。
动态编译(dynamic compiling),在运行时编译 静态编译(static compiling)/提前编译(AOT, ahead-of-time compiling)
JIT是一种动态编译,即在某段代码即将第一次被执行的时候进行编译,因此叫即时编译。
但是实际上 JIT 有以下问题:
JIT基于运行期分析编译,而JavaScript是一个没有类型的语言。大部分时间,JIT 编译器其实是在猜测JavaScript中的类型。
function add(a, b) {
return a + b;
}
let a = add(1, 1);
编译器看到这里,就把add编译成
function add(int a, int b) {
return a + b;
}
结果紧接着,又出现了
let b = add('1', '1');
这时之前的代码已经被编译成机器码了,所以在这种情况下,JIT编译器只能推倒重来。有时JIT带来的性能提升还没有重编的开销大。
Asm.js
既然JIT性能在进一步提升性能上遇到的主要问题是类型不确定,那我们有没有办法可以让用户把类型标注出来呢?
按照这个思路, 催生了两种实现路径:
一种是以 Typescript,Dart为代表的强类型语言。即发明一种强类型语言,然后把它编译成 JavaScript。这种方法可以一定程度地解决前面说过的JIT把类型推翻重来的时间浪费问题。另一种则以 Asm.js为代表。Asm.js是JavaScript的子集。这种方法可以让我们实现AOT。
简介
很多开发者一直没有放弃“在浏览器里运行游戏”这个执念,于是2012年,Mozilla的一位工程师Alon Zakai突发奇想:既然许多3D游戏都是用 C/C++ 语言写的,而JavaScript 的基本语法又与 C 语言高度相似。如果能将 C/C++ 语言编译成JavaScript代码,它们不就能在浏览器里运行了吗?
于是,他开始研究怎么才能实现这个目标,然后就有了一个编译器项目 Emscripten。这个编译器可以将 C / C++ 代码编译成JavaScript代码,但不是普通的 JavaScript,而是一种叫做 Asm.js 的JavaScript变体。
上面这张图是Asm.js的官方定义:extraordinarily optimizable(极度优化的), low-level(底层的), subset(子集)
Asm.js优化的核心用一句话说就是:静态变量类型,取消垃圾回收机制
变量
Asm.js中有两种变量类型:32位带符号整数和64位带符号浮点数
整数与浮点数
let a = 1;
整数 let x = a | 0;浮点数 let y = +a;
声明变量类型的函数:
function add(x, y) {
x = x | 0;
y = y | 0;
return (x + y) | 0;
}
其他数据类型
而诸如布尔值、字符串和对象都存在内存中,通过TypedArray来调用。
const encoder = new TextEncoder();
const uint8Array = encoder.encode("hello world");
console.log(uint8Array);
console.log(uint8Array.buffer);
垃圾回收
Asm.js 没有垃圾回收机制,所有内存操作都由程序员自己控制。Asm.js 通过TypedArray直接读写内存。
const buffer = new ArrayBuffer(32);
const i8 = new Int8Array(buffer);
i8[0] = 12;
console.log(i8[0]);
支持度
一旦引擎发现运行的是Asm.js,就知道这是经过优化的代码,可以跳过语法分析这一步,直接转成汇编语言。另外,浏览器还会调用 WebGL 通过 GPU 执行Asm.js,即Asm.js的执行引擎与普通的JavaScript脚本不同。Asm.js在浏览器里的运行速度,大约是原生代码的50%左右。
但是从下图中我们可以看到,主流浏览器中几乎只有火狐的Asm.js有较好的支持度
虽然浏览器都可以运行Asm.js(of course,它的本质还是JS),但是不是所有的浏览器的JS引擎都优化地来运行它。
在MDN的文档里也不推荐使用Asm.js
JS与WebAssembly执行时间比较
WebAssembly技术也可以将 C/C++ 转换成 JS引擎可以运行的代码。与Asm.js不同的是,Asm.js是文本,而WebAssembly是二进制字节码。因此WebAssembly的运行速度更快、体积更小。
2008年,JIT的出现让JS得以实现从前不可能的事(如Electron),那WebAssembly的出现是否又是一次JS性能的转折点呢?
获取资源
下载执行与JavaScript等效的WebAssembly文件花费的时间更少,因为WebAssembly是二进制文件,体积也就更小。
在网速慢的情况下效果更明显。
解析
浏览器拿到JS代码后,先回把代码解析成AST。当某一段代码要被执行时,AST又被转换为字节码。
而WebAssembly本身就已经是字节码了
编译&优化
对于WebAssembly,编译器不需要在花费时间去判断类型,也不用对执行时不同类型的相同代码编译出多个版本
执行
由于JS是给人设计的,而WebAssembly是为编译器设计的,因此WebAssembly提供了一组对机器来说更为理想的指令。而这些指定可以让代码执行的速度比原来快上10%~800%
垃圾回收
在JS中,JS引擎会使用垃圾回收器来自动进行垃圾回收处理。但是由于开发者并不能控制垃圾回收时机,它可能在非常重要的时间去工作,从而影响性能。
而WebAssembly 根本不支持垃圾回收。内存是手动管理的(就像在C/C++中一样)。虽然这些可能让开发者编程更困难,但它的确提升了性能。
WebAssembly使用
Emscripten
显然,和Asm.js一样,WebAssembly不是让你手写的,而是通过编译器编译C语言等代码生成的。
Emscripten是一个底层为LLVM编译器的编译器项目,可以将C/C++代码编译生成Asm.js和wasm
安装(windows)
需要环境:
PythonGit
克隆
emsdkgit clone https://github.com/juj/emsdk.git进入
emsdk文件夹cd emsdk安装最新的
emsdk./emsdk install --system latest激活
emsdk./emsdk activate latest应用环境变量
./emsdk_env.bat
基本使用
#include <iostream>
using namespace std;
int main()
{
cout << "hello world!" << endl;
}
# 生成 a.out.js
$ emcc hello.c
# 生成 hello.js
$ emcc hello.c -o hello.js
# 生成 hello.html 和 hello.js
$ emcc hello.c -o hello.html
在线工具
除了Emscripten编译器,有一些在线网站也可以帮助我们快速方便地生成wasm文件。
WebAssembly Explorer是一个不错的网站,将代码粘贴到左边一栏后点击COMPILE就可以得到.wasm文件
一个小例子
下面是一个求斐波那契数列的递归函数。
JavaScript
function fib(x) {
if (x <= 0) {
return 0;
}
if (x <= 2) {
return 1;
}
return fib(x - 1) + fib(x - 2);
}
console.time("JS执行速度");
console.log(fib(45));
console.timeEnd("JS执行速度");
C++
#include <iostream>
#include <ctime>
using namespace std;
int fib(int x)
{
if (x <= 0)
return 0;
if (x <= 2)
return 1;
return fib(x - 1) + fib(x - 2);
}
int main()
{
int start = clock();
fib(45);
int end = clock();
// 这里我是在WSL下运行的,clock单位是10^-6。如果在Windows下clock的单位就是ms
cout << double(end - start) / CLOCKS_PER_SEC * 1000 << endl;
return 0;
}
WASM
fetch('http://127.0.0.1:8080/fib.wasm')
.then(res => res.arrayBuffer())
.then(bytes => WebAssembly.compile(bytes))
.then(module => {
const instance = new WebAssembly.Instance(module);
const fib = instance.exports;
console.log(fib);
console.time('webAssembly');
fib._Z3fibi(45);
console.timeEnd('webAssembly');
})
WebAssembly应用
案例
Figma
是一款基于浏览器的多人实时协作 UI 设计工具
在Figma官网上反复提到了“fast”。它以浏览器为基础环境,所以支持 Windows、macOS 以及 Linux 平台,并且有 iOS 端的预览工具。
在过去,Figma使用Asm.js来加快文件读取速度。现在改用 WebAssembly 技术后,这套多功能 UI 设计工具的运行速度又再飙升 3 倍。
Google Earth
支持各大浏览器的 3D 地图,而且运行流畅
2017年10月底,谷歌开始支持让Google Earth在Firefox上运行,其中的关键就是使用了 WebAssembly。
在WebAssembly尚未问世时,能让代码在浏览器原生执行的技术除了Asm.js 外,还有谷歌自家的Native Client。而在谷歌用Native Client的同时也意味着Google Earth 只能在Chrome浏览器中运行。但随着主流浏览器相继支持WebAssembly的情况下,Google Earth团队承诺,要开始Native Client迁移到WebAssembly。
Egret Engine
Egret Engine是白鹭科技开发的知名游戏引擎,它提供了游戏开发所需要的诸多功能,并允许开发者编写的游戏运行在Web浏览器或移动应用的WebView容器中。
在2017年5月时,白鹭引擎宣布使用WebAssembly重新编写了白鹭引擎的渲染核心。而利用 WebAssembly,白鹭引擎可以将HTML 5代码编译为机器码运行,让游戏运行性能提升 300%。以便进一步提升渲染效率。
支持度
未来
取代JS
2017年2月28日,四大主流浏览器一致同意宣布WebAssembly的MVP版本已经完成,它是一个浏览器可以搭载的稳定版本。2019年12月5日,万维网联盟(W3C)宣布WebAssembly 核心规范成为正式标准。WebAssembly出现后,有人说它将会在未来取代JS,也有人说它是Web开发平台的第四种语言。
其实首先WebAssembly不是开发语言。WebAssembly是一整套字节码标准,它是编译后的产物而不是一种开发语言。也就是说没有谁会裸写WebAssembly。所以WebAssembly不是成为了Web平台的第四种语言,而是它为 Web 平台带来了 C++、C#、Rust、Go……等第n种语言。
其次,WebAssembly更不会替代JS。目前为止WebAssembly还不是非常完善,所以它目前的主要作用是作为 JavaScript 生态的有益补充,与JavaScript共存而不是取而代之。但是我们也相信这门非常罕见地由四大浏览器厂商共同宣布会大力支持并实现的技术肯定会有更光明的前景,各种浏览器兼容性问题、性能问题也终究可以得到解决。
恶意使用
参考文章
An Abridged Cartoon Introduction To WebAssembly
asm.js 和 Emscripten 入门教程
WebAssembly應用案例直擊
Half of the websites using WebAssembly use it for malicious purposes