认识 WebAssembly

@fwon 2017-11-22 05:01:56发表于 fwon/blog

自从Brendan Eich用十天时间创造了JavaScript,人们对它的吐槽就从未间断过。众所周知JavaScript是一门动态语言。运行于JavaScript引擎中,我们熟悉的有Mozilla的SpiderMonkey,Safari的JavaScriptCore,Edge的Chakra还有大名鼎鼎的V8。V8引擎将JavaScript的运行效率提升到一个新的level。所以后来的Nodejs也采用V8作为引擎,实现了用js进行后端开发的愿景。

然而JavaScript发展到今天,其语言基因中存在的缺陷并不能得到根本性的改变。比如常见的加法操作

function add(a, b) {
    return a + b;
}

这段代码在浏览器中的运行过程比你想象的复杂。
add在被调用前,js引擎并不能提前预判传入参数的类型,需要在运行时对参数进行如下一连串的类型判断和转换操作。

pic

对js加法运算的详细操作(keng)有兴趣的可以看这篇文章

V8再快也难以逾越语言本身的瓶颈。这种问题是动态语言的弊端,对于此类问题,业界已经出现了非常多的解决方案。

而本文要讲的正是目前最为前沿的一种 ------ WebAssembly

WebAssembly这个概念其实2015年就提出来了,而就在不久之前,四大浏览器厂商,Chrome, Firefox, Edge, Safari 在新版的浏览器中才全部默认支持Webassembly(Chrome, Firefox早于后两者),这种技术很快将在前端高性能开发领域中大放异彩。

WebAssembly是什么?

下面是来自官方的定义:

WebAssembly or wasm is a new portable, size- and load-time-efficient format suitable for compilation to the web.

关键词:”format",WebAssembly 是一种编码格式,适合编译到web上运行。

事实上,WebAssembly可以看做是对JavaScript的加强,弥补JavaScript在执行效率上的缺陷。

  • 它是一个新的语言,它定义了一种AST,并可以用字节码的格式表示。
  • 它是对浏览器的加强,浏览器能够直接理解WebAssembly并将其转化为机器码。
  • 它是一种目标语言,任何其他语言都可以编译成WebAssembly在浏览器上运行。

想象一下,在计算机视觉,游戏动画,视频编解码,数据加密等需要需要高计算量的领域,如果想在浏览器上实现,并跨浏览器支持,唯一能做的就是用JavaScript来运行,这是一件吃力不讨好的事情。而WebAssembly可以将现有的用C,C++编写的库直接编译成WebAssembly运行到浏览器上, 并且可以作为库被JavaScript引用。那就意味着我们可以将很多后端的工作转移到前端,减轻服务器的压力。这是WebAssembly最为吸引人的特性。并且WebAssembly是运行于沙箱中,保证了其安全性。

为什么要有WebAssembly?

如果只是想让C,C++,Java等原生语言编写的模块运行在浏览器上。我们只需要一个转换器,将源语言转换为目标语言JavaScript,而这种技术其实很早就有了。

例如将Java转换成JavaScript的Google Web Toolkit (GWT)

将python转换成JavaScript的pyjamas 等等。

但是这并没有解决JavaScript执行慢的问题,这跟直接用JavaScript来重写代码库是一样的作用。这就是为什么Electron能直接运行Node.js但对比传统桌面应用依然弱鸡的原因。

要理解JavaScript为什么运行慢,就要理解它在引擎中的处理过程。
传统JavaScript在V8引擎中的编译过程是这样的:首先JavaScript会被编译成AST,然后引擎再将AST, 转化为机器语言交给底层执行。

V8的pipeline结构会进一步先将AST转化为一种中间代码,再对中间代码再次生成优化后的机器码,从而实现更快的执行速度。

pic

对于WebAssembly来说,前面的parser, optimize 全部省了,直接编译到机器码。

pic

浏览器通过增加一种语言格式的编译支持,来实现执行效率的突破。

WebAssembly除了运行快之外,其特殊的二进制表示法也大大减小了代码包的大小。同时提升了浏览器的加载速度。

如何使用WebAssembly?

现在你已经能在这些浏览器中使用WebAssembly了。

WebAssembly这么快,但并不意味着JavaScript这门语言要从此绝迹了。

如前面所说,WebAssembly和JavaScript之间是可以相互调用的。

假设我们用C写了这段代码

include <math.h>
int add(int a, int b) {
    return a + b;
}

首先将其转化为wasm文件, 这里运用一个线上的工具 WasmFiddle

将转化的add.wasm下载下来。wasm是一个十六进制表示的字节码。

0061 736d 0100 0000 018b 8080 8000 0260
017f 0060 027f 7f01 7f02 9280 8080 0001
0365 6e76 0a63 6f6e 736f 6c65 4c6f 6700
0003 8280 8080 0001 0104 8480 8080 0001
7000 0005 8380 8080 0001 0001 0681 8080
8000 0007 9080 8080 0002 066d 656d 6f72
7902 0003 6164 6400 010a 9380 8080 0001
8d80 8080 0000 2001 2000 6a22 0110 0020
010b 

由于目前还没支持 <script src=“abc.wasm" type="module" />的引入方式。所以不能直接在html引入,我们可以通过JS fetch来请求文件。

先封装一个fetch方法:

function fetchAndInstantiateWasm (url, imports) {
    return fetch(url)
    .then(res => {
        if (res.ok)
            return res.arrayBuffer();
        throw new Error(`Unable to fetch Web Assembly file ${url}.`);
    })
    .then(bytes => WebAssembly.compile(bytes))
    .then(module => WebAssembly.instantiate(module, imports || {}))
    .then(instance => instance.exports);
}

用定义好的fetchAndInstantiateWasm方法请求add.wasm文件,并在回调中调用C中定义的add方法,成功输出结果15。

fetchAndInstantiateWasm('add.wasm', {})
.then(m => {
    console.log(m.add(5, 10)); // 15
});

同样通过js import,也能够在C中调用js的方法。

fetchAndInstantiateWasm('program.wasm', {
    env: {
      consoleLog: num => console.log(num)
    }
})
.then(m => {
    console.log(m.add(5, 10)); // 15
});

上面在js代码中定义了consoleLog, 并传入了wasm文件,在C中就可以调用consoleLog方法往控制台输出信息,你也可以执行一些你想要的其他操作。

#include<stdio.h>
void consoleLog(int num);
int add(int num1, int num2) {
    int result = num1 + num2;
    consoleLog(result);
    return result;
}

可直接下载demo代码执行查看效果 demo

运行python -m SimpleHTTPServer后访问localhost:8000, 查看log中输出信息。

前面说WebAssembly是一门新的语言,但上面引入的wasm只是一种字节码,是作为其他语言编译的目标语言,完全没有可读性。其实WebAssembly是有自己的语法的,文件格式为wast。下面是add方法编译成的WebAssembly版本。

(module
  (type $FUNCSIG$vi (func (param i32)))
  (import "env" "consoleLog" (func $consoleLog (param i32)))
  (table 0 anyfunc)
  (memory $0 1)
  (export "memory" (memory $0))
  (export "add" (func $add))
  (func $add (param $0 i32) (param $1 i32) (result i32)
    (call $consoleLog
      (tee_local $1
        (i32.add
          (get_local $1)
          (get_local $0)
        )
      )
    )
    (get_local $1)
  )
)

wast是可编辑的,它同样可以直接转化为wasm, 用于浏览器引入。

上面只是一个最简单的例子,实际上利用WebAssembly实现的应用已经可以相当酷炫。

官方展示的demo游戏

还有一个运用webassembly实现的浏览器视频编辑器

和其他类似技术的区别?

asm.js

可能对前端比较关注的同学有听说过asm.js。它是Mozilla开发的一个JavaScript的子集。就是在JavaScript的基础上,加入了静态类型的支持。
asm.js是Mozilla开发的,所以只支持自家浏览器Firefox。当然代码也可以兼容运行于其他浏览器,但是就没有了优化效果。

asm.js 提供一种语法来表示变量类型

var first = 5;
var second = first;

对于上面这段JavaScript代码,在asm.js里是这样写的

var first = 5;
var second = first | 0;

在first后面加上|0,我们就将first标记为32位整数,而被赋值的second也为被定义为32位整数。
在Mozilla引擎编译代码的时候,遇到这些标志就会提前知道变量的类型,提前优化代码。而这些标记也不影响其他引擎的运算结果。

然而说到底它还是JavaScript,只不过我们提前为优化做了准备。代码还是要经过JavaScript Code ->AST->Optimize的过程。
另外asm.js也是支持将C,C++转化为asm.js的,有兴趣的可以参考这里

TypeScript

大家应该也知道微软的TypeScript,TypeScript做的工作其实跟asm.js有点类似,只不过TypeScript是更加High-Level的。他是JavaScript的一个超集,就是在JavaScript的基础上支持了类型和类等语法。并且能直接编译为JavaScript。TypeScript在于能在开发阶段就进行类型检查,保证代码开发效率和安全性。但是从浏览器运行效率上来看并没有优化效果,因为浏览器并不原生支持。

相同功能的还有facabook的Flow,也是在开发阶段加入类型的支持。

结语

目前WebAssembly由W3C WebAssembly Community Group负责开发与标准定制,而该组织的成员正是来自Google, Microsoft, Mozilla等浏览器开发人员。几个大厂同时投入到WebAssembly的开发中,相信不久WebAssembly就会成为一种浏览器网站&应用的通用优化技术。

参考资料

  1. https://medium.com/javascript-scene/what-is-webassembly-the-dawn-of-a-new-era-61256ec5a8f6
  2. https://medium.com/javascript-scene/why-we-need-webassembly-an-interview-with-brendan-eich-7fb2a60b0723
  3. https://www.youtube.com/watch?v=6v4E6oksar0
  4. http://blog.techbridge.cc/2017/06/17/webassembly-js-future/
  5. https://github.com/WebAssembly/design