1.什么是webassembly?

从历史角度讲,虚拟机过去只能加载JavaScript。这对我们而言足够了,因为JavaScript足够强大从而能够解决人们在当今网络上遇到的绝大部分问题。尽管如此,当试图把JavaScript应用到诸如3D游戏、虚拟现实、增强现实、计算机视觉、图像/视频编辑以及大量的要求原生性能的其他领域的时候,我们将会遇到性能问题(上面抄自mdn)。

webassembly我们从语意上分析是web + assembly,就是在web上应用的汇编语言,它的的出现并不是代替javascript,我认为他们两个是互补的关系,在强计算的部分用webassembly,而与页面的交互可以利用javascript的简单和易用性。

webassembly是继css,html,javascript之外的第四个w3c标准,webassembly的出现并不是希望开发者直接使用webassembly来编写代码,而是希望开发者能够将其他的高级语言例如C/C++/Rust编译成webassembly。 从目前的发展来看,所有的浏览器和node.js(甚至v8)中都已经拥有webassembly的虚拟机了,也就是在这些环境下都支持webassembly运行。

2.为什么用webassembly

1.使 “原生” 模块不那么复杂

运行时 (例如 Node 或 Python 的 CPython) 通常允许你使用低级语言 (例如 C++) 编写模块。这是因为这些低级语言的运行速度通常要快得多。因此,你可以在 Node 中使用本地模块,或者在 Python 中使用扩展模块。但是这些模块通常很难用,因为它们需要在用户设备上进行编译。借助 WebAssembly 的 “原生” 模块,你可以获得差不多的速度而规避复杂化。

2.更容易的沙箱化运行原生代码

另一方面,类似于 Rust 这样的低级语言不会指望 WebAssembly 来提升运行速度。但他们会为了安全性使用 WebAssembly。正如我们在 WASI 公告中所讨论的那样,WebAssembly 默认为你提供轻量级沙箱。因此,像 Rust 这样的语音可以通过 WebAssembly 来沙箱化原生代码模块。

3.跨平台共享原生代码

如果开发人员可以跨不同平台 (例如,在 Web 和桌面应用程序) 共享同一代码库,则可以节省开发时间并降低维护成本。脚本语言和低级语言都是如此。WebAssembly 为你提供了一种在不降低这些平台性能的前提下实现此目标的方法。

3.webassembly vs javascript

js运行速度的发展如下图,在2008年的时候,引入了JIT技术,这使得javascript的速度有将近10倍的提升,这使得我们可以用js来写server端的内容。而在2017年,webassembly出现了,这将又是一个转折点

js-speed

3.1 v8中的JIT(just in time)

要说到webassembly为什么快,要先说下javascript的运行原理和JIT技术,众所周知的是javascript是一门高级语言,如果想要让机器看懂这门语言,你需有一个“翻译器”----Interpreter或者是Compiler来将高级语言翻译成机器语言,简单理解interpreter是一个实时翻译机,像是同声传译,而compiler是将高级语言提前全部翻译好,再交给机器,javascript是一门interpreted language,意味着这个翻译过程是“on the fly”的,一行一行翻译,一行一行执行。而像c++这种语言是利用compiler来提前翻译好,再运行。

Interpreter优点:那么这两种翻译模式肯定是各有好坏,“实时翻译”的好处就是启动快,因为你不需要提前编译它,可以边走边唠(可能这就是大部分开发者觉得js简单的原因之一吧,你在浏览器打个 1 + 1就能快速得出结果)
Interpreter缺点:但是坏处就是比方说你有一个for循环,意味着你要一遍一遍的翻译同一行代码,做同样的事,而无法做一些优化。
Compiler优点: “提前翻译”的好处当然就是因为我们提前翻译好了,在运行之前可以将代码做下优化,这样就加快了运行时候的速度。
Compiler缺点: 坏处当然就是启动太慢了,要先编译。
Compiler optimization

那么JIT技术就是在原来JS只有Interpreter技术之上加入了Compiler,作为成年人我不选择,我两个都要!在用interpreter实时翻译的过程当中加入了compiler的一些特性,比方说有那么一行代码在被执行了很多次的时候,js引擎会将其设置为“Warm”,进行一系列超级优化,再被执行的时候就设置为“Hot”,进行究极优化(听说数码宝贝出新一代了?)。

Type specialization

再比如js中因为类型都是动态的,就是一个array中每个元素的类型都是不一定的,可能是object可能是string可能是number,意味着当你iterate这个array的每个元素都要进行一系列的类型检查啥的。当JIT技术引进以后,我们的js引擎可能会在跑到前十个元素都是number的情况下,大胆预测你后面也都是number(当然之后会进行检查)。

3.2 为什么webassembly要更快

Ok,说完js的JIT之后,我们来总结下执行js和webassembly的过程:

js-speed
  1. 首先看fetching部分,webassembly代码更加compact,因为js要更human-readable,所以代码提及相较于汇编语言体积会更大
  2. parse/decode过程中,js需要先生成ast,然后再通过ast生成IR(intermediate representation),IR再生成机器码(x86或者arm)。而webassembly则不需要这个转化过程,这里盗张图,看下图。可知webassembly能够直接生成机器密码。
IR
  1. compile + optimize的过程在JIT部分说过,js引擎要边运行边进行优化,比方说watch数据的类型变化啦之类的,而webassembly更接近底层机器码,数据类型什么的是固定的
  2. 此外JS中还有reoptimization的过程,在JIT的type specialization部分说到了js引擎为了提升性能的类型预测部分,肯定有failed的时候,所以js执行中还需要有reoptimization的部分。
  3. 至于execution的部分,因为webassembly更底层(webassembly的两种格式看下面一小节,wasm和wat格式)相比于js即使最终都生成机器码,webassembly开发者写的代码所能优化的程度一定是大于js所能做的优化。
  4. GC过程容易理解,能够转换成webassembly的高级语言中gc都需要开发者手动处理,而js中是自动处理。

3.3 webassembly长什么样?(wasm格式和wat格式)

讲了这么多,webassembly到底长什么样子呢?
先写一个简单的c++方法如下:

// test.c
    int addAll(int times) {
        int n = 0;
        for (int i = 0; i < times; ++i) {
           n += i;
        };
        return n;
    }
  1. wasm后缀的文件:直接放到webassembly虚拟机上的可执行文件格式,我们利用Emscripten工具将上面的c++文件生成一个wasm文件,命令为:emcc -O3 -s "EXPORTED_FUNCTIONS=['_addAll']" -o test.wasm test.c --no-entry ,就是在wasm文件中导出addAll方法,生成的wasm就是一个二进制(实际上是16进制)文件。打开text.wasm文件内容如下:
00 61 73 6D 0D 00 00 00 01 86 80 80 80 00 01 60
01 7F 01 7F 03 82 80 80 80 00 01 00 04 84 80 80
80 00 01 70 00 00 05 83 80 80 80 00 01 00 01 06
81 80 80 80 00 00 07 96 80 80 80 00 02 06 6D 65
6D 6F 72 79 02 00 09 5F 5A 35 61 64 64 34 32 69
00 00 0A 8D 80 80 80 00 01 87 80 80 80 00 00 20
00 41 2A 6A 0B
  1. wat后缀的文件:方便人类可读的文件,并且可以在此格式上进行coding,利用wasm2wat工具将生成的wasm转成wat格式,wasm2wat test.wasm生成的wat的内容如下:可以看出更human-readable。简单介绍下,webassembly中只支持四种数据类型
  • i32: 32-bit 整型
  • i64: 64-bit 整型
  • f32: 32-bit 浮点型
  • f64: 64-bit 浮点型
    像是i32.sub这样是运算指令,前面两个推入栈中的变量为运算指令的两个参数,看的出这个指令是够精简的。如果对手写wasm感兴趣的同学可以看这篇内容
    (module
  (type (;0;) (func (result i32)))
  (type (;1;) (func (param i32) (result i32)))
  (type (;2;) (func))
  (type (;3;) (func (param i32)))
  (func (;0;) (type 2)
    nop)
  (func (;1;) (type 1) (param i32) (result i32)
    local.get 0
    i32.const 1
    i32.lt_s
    if  ;; label = @1
      i32.const 0
      return
    end
    local.get 0
    i32.const 1
    i32.sub
    i64.extend_i32_u
    local.get 0
    i32.const 2
    i32.sub
    ...
    // 篇幅原因,只展示部分

当然wasm和wat格式之间是可以相互转换的,看你是要读它还是用它。

3.4 webassembly和js中的通讯

上一个小结我们说了在webassembly中只有四个数据类型,并且都是数字类型,我们的c++累加函数中只传递了数字n,代表累加的次数,那么我们如何在webassembly和js之间传递复杂的数据结构比方说string或者object呢?答案就是传递内存的buffer,有点像是在两者这件传递一个指针,这个指针是数字类型,然后把要传递的内容放到这片共享的内存中。这里详情可以看mdn的WebAssembly.Memory api,这里的内容复制自这里

connmunication
    //创建一个6.4MiB大的内存
    const wasmMemory = new WebAssembly.Memory({initial:10, maximum:100}) 
    WebAssembly.instantiate(wasmBinary, {
      env: {
        // 告诉webassembly这片内存咱俩一起用
        memory: wasmMemory
      }
    })

在js中,可以通过typedArray来操作二进制数据buffer

// 把想要传递的数据转成 ArrayBuffer (假设是 Uint8Array)
const dataBuffer = encodeDataByJS({ /* my data */ })
// 向 wasm 申请一段内存,由 wasm 代码负责实现并返回内存内存起始地址
const offset = applyMemoryFormWasm(dataBuffer.length)
// 以 unit8 的格式操作 wasm 的内存 (格式应该与 dataBuffer 的格式相同)
const heapUint8 = new Uint8Array(wasmMemory.buffer, offset, dataBuffer.length)
// 把数据写入 wasm 内存
heapUint8.set(dataBuffer)

4.写一个webassembly的demo

4.1 c++实现一个累加函数

我们用上面的demo实现一个1 + 2 + 3 + ... + n的c++函数

4.2 用emscripten生成.wasm文件

上部分提到利用emscripten生成test.wasm文件

4.3 如何在js中调用生成的.wasm文件

我们尝试在node中使用这个wasm文件导出的addAll方法如下:

fs.readFile('./test.wasm', (err, data) => {
    if (err) throw err;
    WebAssembly.instantiate(data).then((module) => {
            console.log('In the native WebAssembly function')
            console.time('performance1')
            console.log(module.instance.exports.addAll(50000))
            console.timeEnd('performance1')
        });
    })

因为现在webassembly已经是web标准,各种js引擎中都有相应的api(WebAssembly)调用webassembly。使用方法很简单,有兴趣的童鞋去mdn搜一下相关的其他信息。

4.4粗略比较下计算性能

     function addAll(n) {
        var result = 0
        console.log('In the Javascript')
        console.time('performance2')
        for(let i = 0 ; i < n; i++) {
            result+=i
        }
        console.log(result)
        console.timeEnd('performance2')
}

js和webassembly执行0 + 1 + 2 + ... + 50000的耗时在我的电脑上为webassembly:0.07ms;js:4.886ms;看的出这么一个简单的计算就能看出来其性能差别了。

5.webassembly的runtime

wasmer

除了浏览器和node之外,我们说两个webassembly的runtime,第一个是wasmer,官网说这个runtime可以跑在任意的设备之上,看了下它更像是一个docker容器,能够运行wasm。用它来运行我们的累加wasm:

wasmer


此外wasmr还提供了一个工具叫做wapm,它有点像是npm,在其社区上一些用户会上传自己编译好的wasm工具包,你只需要下载下来就能够直接在wasmer跑起来

wapm


在wapm的项目之下甚至会有一个wapm_packages和一个.lock的文件,内容也和我们的package.lock类似。

wapm-lock

wasm-micro-runtime

这个runtime是由intel的中国团队开发,其目的就是运行在iot设备之上,其支持的平台架构有:

  1. X86-64, X86-32
  2. ARM, THUMB (ARMV7 Cortex-M7 and Cortex-A15 are tested)
  3. AArch64 (Cortex-A57 and Cortex-A53 are tested)
  4. MIPS
  5. XTENSA
    按照教程本地编译出了runtime并且能够成功运行我们的累加wasm方法:
iwasm


生成的runtime命令行文件iwasm只有212k

6.webassembly现在的应用场景:

在w3c 2020年8月29日的线上会议中,几家技术大厂分别介绍了他们用webassembly的场景:

  1. 典型场景一:B站,在up主上传视频的时候利用webassembly进行视频内容的检查,根据视频生成推荐封面,这些操作都是在用户的浏览器(前端)实现的。
wasm-intel
  1. intel在嵌入式设备上的应用,用Webassembly实现了一套应用框架:
wasm-intel
  1. Unity游戏引擎也有webassembly的实现
  2. Emulator(仿真器)例如game boy emulator
  3. 一些媒体处理网站,squoosh、ogv.js、Photon等。

reference:

本文与https://quickapp.vivo.com.cn/ 中的文章为同一作者

  1. https://hacks.mozilla.org/2017/02/a-cartoon-intro-to-webassembly/
  2. https://mp.weixin.qq.com/s/yZmci4krPkxA8uEfaJh5XQ
  3. https://developer.aliyun.com/article/740902