WebAssembly完全入门:了解wasm的前世今身

接触 WebAssembly 之后,在 google 上看了很多资料。然而,这些资料对 WebAssembly 的使用、介绍、意义等方面的解释都比较模糊和笼统,总是令人感觉没有获得预期的收获,要么是因为文章中的例子自己去实操不能成功,要么就是不知所云、一脸蒙蔽。本着业务催生技术的态度,这篇文章就此诞生了。本文主要是对 WebAssembly 的背景做一些介绍,包括 WebAssembly 是怎么出现的?优势在哪儿?等等。

WebAssembly 是什么?

定义

首先我们给它下个定义。

WebAssembly 或者 wasm 是一个可移植、体积小、加载快并且兼容 Web 的全新格式。

例子

当然,我知道,即使你看了定义也不知道 WebAssembly 到底是什么东西。废话不多说,我们通过一个简单的例子来看看 WebAssembly 到底是什么。

上图的左侧是用 C++ 实现的求递归的函数。中间是十六进制的 Binary Code。右侧是指令文本。可能有人就问,这跟 WebAssembly 有个屁的关系?其实,中间的十六进制的 Binary Code 就是 WebAssembly。

编译目标

大家可以看到,其可写性和可读性差到无法想象。那是因为 WebAssembly 不是用来给各位用手 一行一行撸 的代码,WebAssembly 是一个 编译目标。什么是编译目标?当我们写 TypeScript 的时候,Webpack 最后打包生成的 JavaScript 文件就是编译目标。可能大家已经猜到了,上图的 Binary 就是左侧的 C++ 代码经过编译器编译之后的结果。

WebAssembly 的由来

性能瓶颈

在业务需求越来越复杂的现在,前端的开发逻辑越来越复杂,相应的代码量随之变的越来越多。相应的,整个项目的起步的时间越来越长。在性能不好的电脑上,启动一个前端的项目甚至要花上十多秒。这些其实还好,说明前端越来越受到重视,越来越多的人开始进行前端的开发。

但是除了逻辑复杂、代码量大,还有另一个原因是 JavaScript 这门语言本身的缺陷,JavaScript 没有静态变量类型。这门解释型编程语言的作者 Brendan Eich,仓促的创造了这门如果被广泛使用的语言,以至于 JavaScript 的发展史甚至在某种层面上变成了填坑史。为什么说没有静态类型会降低效率。这会涉及到一些 JavaScript 引擎的一些知识。

静态变量类型所带来的问题

这是 Microsoft Edge 浏览器的 JavaScript 引擎 ChakraCore 的结构。我们来看一看我们的 JavaScript 代码在引擎中会经历什么。- JavaScript 文件会被下载下来。

  • 然后进入 Parser,Parser 会把代码转化成 AST(抽象语法树)。
  • 然后根据抽象语法树,Bytecode Compiler 字节码编译器会生成引擎能够直接阅读、执行的字节码。
  • 字节码进入翻译器,将字节码一行一行的翻译成效率十分高的 Machine Code。

在项目运行的过程中,引擎会对执行次数较多的 function 记性优化,引擎将其代码编译成 Machine Code 后打包送到顶部的 Just-In-Time(JIT) Compiler,下次再执行这个 function,就会直接执行编译好的 Machine Code。但是由于 JavaScript 的动态变量,上一秒可能是 Array,下一秒就变成了 Object。那么上一次引擎所做的优化,就失去了作用,此时又要再一次进行优化。

asm.js 出现

所以为了解决这个问题,WebAssembly 的前身,asm.js 诞生了。asm.js 是一个 Javascript 的严格子集,合理合法的 asm.js 代码一定是合理合法的 JavaScript 代码,但是反之就不成立。同 WebAssembly 一样,asm.js 不是用来给各位用手 一行一行撸 的代码,asm.js 是一个 编译目标。它的可读性、可读性虽然比 WebAssembly 好,但是对于开发者来说,仍然是无法接受的。

asm.js 强制静态类型,举个例子。

function asmJs() {
    'use asm';

    let myInt = 0 | 0;
    let myDouble = +1.1;
}

为什么 asm.js 会有静态类型呢?因为像0 | 0这样的,代表这是一个 Int 的数据,而+1.1则代表这是一个 Double 的数据。

asm.js 不能解决所有的问题

可能有人有疑问,这问题不是解决了吗?那为什么会有 WebAssembly?WebAssembly 又解决了什么问题?大家可以再看一下上面的 ChakraCore 的引擎结构。无论 asm.js 对静态类型的问题做的再好,它始终逃不过要经过 Parser,要经过 ByteCode Compiler,而这两步是 JavaScript 代码在引擎执行过程当中消耗时间最多的两步。而 WebAssembly 不用经过这两步。这就是 WebAssembly 比 asm.js 更快的原因。

WebAssembly 横空出世

所以在 2015 年,我们迎来了 WebAssembly。WebAssembly 是经过编译器编译之后的代码,体积小、起步快。在语法上完全脱离 JavaScript,同时具有沙盒化的执行环境。WebAssembly 同样的强制静态类型,是 C/C++/Rust 的编译目标。

WebAssembly 的优势

WebAssembly 和 asm.js 性能对比

下面的图是 Unity WebGL 使用和不使用 WebAssembly 的起步时间对比的一个 BenchMark,给大家当作一个参考。可以看到,在 FireFox 中,WebAssembly 和 asm.js 的性能差异达到了 2 倍,在 Chrome 中达到了 3 倍,在 Edge 中甚至达到了 6 倍。通过这些对比也可以从侧面看出,目前所有的主流浏览器都已经支持 WebAssembly V1(Node >= 8.0.0)。

与 JavaScript 做对比

我自己在一个用create-react-app新建的项目中,分别对比了 WebAssembly 版本和原生 JavaScript 版本的递归无优化的 Fibonacci 函数,下图是这两个函数在值是 45、48、50 的时候的性能对比。

看图说话,这就是 WebAssembly 与 JavaScript 很实际的一个性能对比。几乎稳定的是 JavaScript 的两倍。

WebAssembly 在大型项目中的应用

在这里能够举的例子还是很多,比如 AutoCAD、GoogleEarth、Unity、Unreal、PSPDKit、WebPack 等等。拿其中几个来简单说一下。

AutoCAD

这是一个用于画图的软件,在很长的一段时间是没有 Web 的版本的,原因有两个,其一,是 Web 的性能的确不能满足他们的需求。其二,在 WebAssembly 没有面世之前,AutoCAD 是用 C++ 实现的,要将其搬到 Web 上,就意味着要重写他们所有的代码,这代价十分的巨大。

而在 WebAssembly 面世之后,AutoCAD 得以利用编译器,将其沉淀了 30 多年的代码直接编译成 WebAssembly,同时性能基于之前的普通 Web 应用得到了很大的提升。正是这些原因,得以让 AutoCAD 将其应用从 Desktop 搬到 Web 中。

Google Earth

Google Earth 也就是谷歌地球,因为需要展示很多 3D 的图像,对性能要求十分高,所以采取了一些 Native 的技术。最初的时候就连 Google Chrome 浏览器都不支持 Web 的版本,需要单独下载 Google Earth 的 Destop 应用。而在 WebAssembly 之后呢,谷歌地球推出了 Web 的版本。而据说下一个可以运行谷歌地球的浏览器是 FireFox。

Unity 和 Unreal 游戏引擎

这里给两个油管的链接自己体验一下。

WebAssembly 要取代 JavaScript?

答案是否定的,请看下图。

大家可以看到这是一个协作关系。WebAssembly 是被设计成 JavaScript 的一个完善、补充,而不是一个替代品。WebAssembly 将很多编程语言带到了 Web 中。但是 JavaScript 因其不可思议的能力,仍然将保留现有的地位。

什么时候使用 WebAssembly?

说了这么多,我到底什么时候该使用它呢?总结下来,大部分情况分两个点。

  • 对性能有很高要求的 App/Module/ 游戏。
  • 在 Web 中使用 C/C++/Rust/Go 的库举个简单的例子。如果你要实现的 Web 版本的 Ins 或者 Facebook, 你想要提高效率。那么就可以把其中对图片进行压缩、解压缩、处理的工具,用 C++ 实现,然后再编译回 WebAssembly。

WebAssembly 的几个开发工具

WebAssembly 的意义

在我的个人理解上,WebAssembly 并没有要替代 JavaScript,一统天下的意思。我总结下来就两个点。

  • 给了 Web 更好的性能。
  • 给了 Web 更多的可能关于 WebAssembly 的性能问题,之前也花了很大的篇幅讲过了。而更多的可能,随着 WebAssembly 的技术越来越成熟,势必会有更多的应用,从 Desktop 被搬到 Web 上,这会使本来已经十分强大的 Web 更加丰富和强大。

WebAssembly 实操

要进行这个实际操作,你需要安装上文提到过的编译器 Emscripten,然后按照 这个步骤去安装。以下的步骤都默认为你已经安装了 Emscripten。

安装步骤: http://webassembly.org.cn/getting-started/developers-guide/

WebAssembly 在 Node 中的应用

导入 Emscripten 环境变量

进入到你的 emscripten 安装目录,执行以下代码。

source emsdk/emsdk_env.sh

新建 C 文件

用 C 实现一个求和文件test.c,如下。

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

使用 Emscripten 编译 C 文件

在同样的目录下执行如下代码。

emcc test.c -Os -s WASM=1 -s SIDE_MODULE=1 -o test.wasm

emcc就是 Emscripten 编译器,test.c是我们的输入文件,-Os表示这次编译需要优化,-s WASM=1表示输出 wasm 的文件,因为默认的是输出 asm.js,-s SIDE_MODULE=1表示就只要这一个模块,不要给我其他乱七八糟的代码,-o test.wasm是我们的输出文件。

编译成功之后,当前目录下就会生成test.wasm

编写在 Node 中调用的代码

新建一个 js 文件test.js。代码如下。

const fs = require('fs');
let src = new Uint8Array(fs.readFileSync('./test.wasm'));
const env = {
    memoryBase: 0,
    tableBase: 0,
    memory: new WebAssembly.Memory({
        initial: 256
    }),
    table: new WebAssembly.Table({
        initial: 2,
        element: 'anyfunc'
    }),
    abort: () => {throw 'abort';}
}
WebAssembly.instantiate(src, {env: env})
.then(result => {
    console.log(result.instance.exports._add(20, 89));
})
.catch(e => console.log(e));

执行 test.js

运行以下代码。

node test.js

然后就可以看到输出的结果 109 了。

WebAssembly 在 React 当中的应用

通过 fetch 的方法调用 直接用 fetch 的方式。大概的调用方式如下。

const fibonacciUrl = './fibonacci.wasm';
const {_fibonacci} = await this.getExportFunction(fibonacciUrl);

getExportFunction具体代码如下。

getExportFunction = async (url) => {
    const env = {
      memoryBase: 0,
      tableBase: 0,
      memory: new WebAssembly.Memory({
        initial: 256
      }),
      table: new WebAssembly.Table({
        initial: 2,
        element: 'anyfunc'
      })
    };
    const instance = await fetch(url).then((response) => {
      return response.arrayBuffer();
    }).then((bytes) => {
      return WebAssembly.instantiate(bytes, {env: env})
    }).then((instance) => {
      return instance.instance.exports;
    });
    return instance;
};

通过 import C 文件来调用

先通过 Import 的方式来引进依赖。

import wasmC from './add.c';

然后进行调用。具体的方式如下。

wasmC({
  'global': {},
  'env': {
    'memoryBase': 0,
    'tableBase': 0,
    'memory': new WebAssembly.Memory({initial: 256}),
    'table': new WebAssembly.Table({initial: 0, element: 'anyfunc'})
  }
}).then(result => {
  const exports = result.instance.exports;
  const add = exports._add;
  const fibonacci = exports._fibonacci;
  console.log('C return value was', add(2, 5643));
  console.log('Fibonacci', fibonacci(2));
});

详细的代码在这里: https://github.com/detectiveHLH/webassembly-in-react

写在后面

如今技术出现的越来越多,但是实际上在工作中能够用到的,越并不是那么多。其实很多大厂所输出的一些技术,都是有业务场景的,有业务做推动。而不是凭空造轮子。所以总结下来适合自己的才是最好的。当然不是说不要了解新技术,了解新技术跟上步伐是十分必要的。我们现在不用,不代表不需要了解。相反,以后再遇到类似的业务场景时,我们就会多一种选择,可以更加从容的对待。

本文转载自微信公众号:SH的全栈笔记

  • 发表于:
  • 本文为 InfoQ 中文站特供稿件
  • 首发地址https://www.infoq.cn/article/lwlcldGJyC7lye95EWl8

扫码关注云+社区

领取腾讯云代金券