webassembly 的那些事(转)

2018-01-24 20:00
作者:刘艳
https://mp.weixin.qq.com/s/lFqvdBvusCcndoBK0-U7kg

简介

JS于1995年问世,设计的初衷不是为了执行起来快。直到08年性能大战中,许多浏览器引入了即时编译 JIT(just-in-time编译器),Java 代码的运行渐渐变快。正是由于这些 JIT 的引入,使得 Java 的性能达到了一个转折点,JS 代码执行速度快了 20 -- 50倍。

JIT 是使 Java 运行更快的一种手段,通过监视代码的运行状态,把 hot 代码(重复执行多次的代码)进行优化。通过这种方式,可以使 Java 应用的性能提升很多倍。

更多JIT工作原理,有兴趣请移步:https://zhuanlan.zhihu.com/p/25669120

随着性能的提升,Java 可以应用到以前根本没有想到过的领域,比如用于后端开发的 Node.js。性能的提升使得 Java 的应用范围得到很大的扩展。

Java的无类型是Java引擎的性能瓶颈之一,在过去几年,我们看到越来越多的项目问世,它们试图通过开发编译程序,将其他语言代码转化为 Java,以此让开发者克服 Java 自身存在的一些短板。其中一些项目专注于给编程语言增加新的功能,比如微软的 Type 和 Google 的 Dart,【设计一门新的强类型语言并强制开发者进行类型指定】或是加快 Java 的执行速度,例如 Mozilla 的 asm.js 项目和Google的PNaCI【给现有的Java加上变量类型】。

现在通过 WebAssembly,我们很有可能正处于第二个拐点。

什么是webAssembly?

WebAssembly是一种新的适合于编译到Web的,可移植的,大小和加载时间高效的格式,是一种新的字节码格式。它的缩写是".wasm",.wasm 为文件名后缀,是一种新的底层安全的“二进制”语法。它被定义为“精简、加载时间短的格式和执行模型”,并且被设计为Web 多编程语言目标文件格式。这意味着浏览器端的性能会得到极大提升,它也使得我们能够实现一个底层构建模块的集合.

webAssembly的优势

webassembly相较于asm.js的优势主要是涉及到性能方面。根据WebAssembly FAQ的描述:在移动设备上,对于很大的代码库,asm.js仅仅解析就需要花费20-40秒,而实验显示WebAssembly的加载速度比asm.js快了20倍,这主要是因为相比解析 asm.js 代码,Java 引擎破译二进制格式的速度要快得多。

主流的浏览器目前均支持webAssembly。

Safari 支持 WebAssembly的第一个版本是11
Edge 支持 WebAssembly的第一个版本是16
Firefox 支持 WebAssembly的第一个版本是 52
chrome 支持 WebAssembly的第一个版本是 57

使用WebAssembly,我们可以在浏览器中运行一些高性能、低级别的编程语言,可用它将大型的C和C++代码库比如游戏、物理引擎甚至是桌面应用程序导入Web平台。

开发前准备工作(MAC系统)

1.安装 cmake brew install cmake

2.安装 pyhton brew insatll python

3.安装 Emen (调整下电脑的休眠时间,不要让电脑进入休眠,安装时间较长)

安装步骤如下:

git clone https://github.com/juj/emsdk.git
cd emsdk
./emsdk install --build=Release sdk-incoming-64bit binaryen-master-64bit
./emsdk activate --global --build=Release sdk-incoming
-64bit binaryen-master-64bit

执行 source ./emsdkenv.sh,并将shell中的内容添加到环境变量中(~/.bashprofile):

export PATH="/Users/liuyan/Work/emsdk:/Users/liuyan/Work/emsdk/clang/fastcomp/build_incoming_64/bin:/Users/liuyan/Work/emsdk/node/8.9.1_64bit/bin:/Users/liuyan/Work/emsdk/emen/incoming:/Users/liuyan/Work/emsdk/binaryen/master_64bit_binaryen/bin:/Library/Frameworks/Python.framework/Versions/2.7/bin:/Users/liuyan/.nvm/versions/node/v8.4.0/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Users/liuyan/Library/Android/sdk/tools:/Users/liuyan/Library/Android/sdk/platform-tools"
export EMSDK="/Users/liuyan/Work/emsdk"
export EM_CONFIG="/Users/liuyan/.emen"
export EMEN="/Users/liuyan/Work/emsdk/emen/incoming"
export BINARYEN_ROOT="/Users/liuyan/Work/emsdk/binaryen/master_64bit_binaryen"

执行: source ~/.bash_profile

4.安装 WABT(将.wast文件转成 .wasm文件)

git clone https://github.com/WebAssembly/wabt.git
cd wabt
make install gcc-release

5.浏览器设置

Chrome: 打开 chrome://flags/#enable-webassembly,选择 enable。
Firefox: 打开 about:config 将 java.options.wasm 设置为 true。

如果浏览器太旧,请更新浏览器,或者安装激进版浏览器来体验新技术。

6.一个本地web服务器.

Emen,它基于 LLVM ,可以将 C/C++ 编译成 asm.js,使用 WASM 标志也可以直接生成 WebAssembly 二进制文件(后缀是 .wasm)

Emen
source.c -----> target.js
Emen (with flag)
source.c -----> target.wasm

注:emcc 在 1.37 以上版本才支持直接生成 wasm 文件

Binaryen 是一套更为全面的工具链,是用C++编写成用于WebAssembly的编译器和工具链基础结构库。WebAssembly是二进制格式(Binary Format)并且和Emen集成,因此该工具以Binary和Em-en的末尾合并命名为Binaryen。它旨在使编译WebAssembly容易、快速、有效。

wasm-as:将WebAssembly由文本格式编译成二进制格式;
wasm-dis:将二进制格式的WebAssembly反编译成文本格式;
asm2wasm:将asm.js编译到WebAssembly文本格式,使用Emen的asm优化器;
s2wasm:在LLVM中开发,由新WebAssembly后端产生的.s格式的编译器;
wasm.js:包含编译为Java的Binaryen组件,包括解释器、asm2wasm、S表达式解析器等。

WABT工具包支持将二进制WebAssembly格式转换为可读的文本格式。其中wasm2wast命令行工具可以将WebAssembly二进制文件转换为可读的S表达式文本文件。而wast2wasm命令行工具则执行完全相反的过程。

wat2wasm: webAssembly文本格式转换为webAssembly二进制格式(.wast 到 .wasm)
wasm2wat: 将WebAssembly二进制文件转换为可读的S表达式文本文件(.wat)
wasm-objdump: print information about a wasm binary. Similiar to objdump.
wasm-interp: 基于堆栈式解释器解码和运行webAssembly二进制文件
wat-desugar: parse .wat text form as supported by the spec interpreter
wasm-link: simple linker for merging multiple wasm files.
wasm2c: 将webAssembly二进制文件转换为C的源文件

webAssembly的方法 webAssembly.validate

webAssembly.validate() 方法验证给定的二进制代码的 typed array 是否是合法的wasm module.返回布尔值。

WebAssembly.validate(bufferSource);

使用

java
fetch('xxx.wasm').then(response =>
response.arrayBuffer()
).then(function(bytes) {
var valid = WebAssembly.validate(bytes); //true or false
});

webAssembly.Module

WebAssembly.Module() 构造函数可以用来同步编译给定的 WebAssembly 二进制代码。不过,获取 Module 对象的主要方法是通过异步编译函数,如 WebAssembly.compile(),或者是通过 IndexedDB 读取 Module 对象.

var myModule = new WebAssembly.Module(bufferSource);

参数: 一个包含你想编译的wasm模块二进制代码的 typed array(类型数组) or ArrayBuffer(数组缓冲区).

重要提示:由于大型模块的编译可能很消耗资源,开发人员只有在绝对需要同步编译时,才使用 Module() 构造函数;其他情况下,应该使用异步 WebAssembly.compile() 方法。

webAssembly.compile

WebAssembly.compile() 方法编译WebAssembly二进制代码到一个WebAssembly.Module 对象。

Promise<WebAssembly.Module> WebAssembly.compile(bufferSource);

webAssembly.Instance

WebAssembly.Instance实例对象是有状态,可执行的 WebAssembly.Module实例。实例中包含了所有可以被 Java调用的WebAssembly 代码导出的函数。

重要提示:由于大型模块的实例化可能很消耗资源,开发人员只有在绝对需要同步编译时,才使用 Instance() 构造函数;其他情况下,应该使用异步 WebAssembly.instantiate()方法。

var myInstance = new WebAssembly.Instance(module, importObject);

module: 需要被实例化的webAssembly module
importObject: 需要导入的变量

webAssembly.instantiate

Promise<WebAssembly.Instance> WebAssembly.instantiate(module, importObject);

webAssembly.Memory

当 WebAssembly 模块被实例化时,它需要一个 memory 对象。你可以创建一个新的WebAssembly.Memory并传递该对象。如果没有创建 memory 对象,在模块实例化的时候将会自动创建,并且传递给实例。

var myMemory = new WebAssembly.Memory(memoryDeor);

memoryDeor (object)

initial
maximum 可选

webAssembly.Table

var myTable = new WebAssembly.Table(tableDeor);

tableDeor (object)

element,当前只支持一个值。 'anyfunc'
initial, WebAssembly Table的初始元素数
maximum(可选), 允许的最大元素数

webAssembly使用

WebAssembly 与其他的汇编语言不一样,它不依赖于具体的物理机器。可以抽象地理解成它是概念机器的机器语言,而不是实际的物理机器的机器语言。浏览器把 WebAssembly 下载下来后,可以迅速地将其转换成机器汇编代码。

快速体验webAssembly

WebAssembly.compile(new Uint8Array(`
00 61 73 6d 01 00 00 00 01 0c 02 60 02 7f 7f 01
7f 60 01 7f 01 7f 03 03 02 00 01 07 10 02 03 61
64 64 00 00 06 73 71 75 61 72 65 00 01 0a 13 02
08 00 20 00 20 01 6a 0f 0b 08 00 20 00 20 00 6c
0f 0b`.trim().split(/[srn]+/g).map(str => parseInt(str, 16))
)).then(module => {
const instance = new WebAssembly.Instance(module)
//使用 WebAssembly.Instance 将模块对象转成 WebAssembly 实例
const { add, square } = instance.exports
//通过 instance.exports 可以拿到 wasm 代码输出的接口
console.log('2 + 4 =', add(2, 4))
console.log('3^2 =', square(3))
console.log('(2 + 5)^2 =', square(add(2 + 5)))
})

使用C/C++

hello.c

#include <stdio.h>
int main(int argc, char ** argv) {
printf("Hello Worldn");
return 0;
}

编译:

emcc hello.c -s WASM=1 -o hello.html

-s WASM=1 — 指定我们想要的wasm输出形式。如果我们不指定这个选项,Emen默认将只会生成asm.js。
-o hello.html — 指定这个选项将会生成HTML页面来运行我们的代码,并且会生成wasm模块以及编译和实例化wasim模块所需要的“胶水”js代码,这样我们就可以直接在web环境中使用了。

编译后

二进制的wasm模块代码 (hello.wasm)
一个包含了用来在原生C函数和Java/wasm之间转换的胶水代码的Java文件 (hello.js)一个用来加载,编译,实例化你的wasm代码并且将它输出在浏览器显示上的一个HTML文件 (hello.html)

调用C++中的方法

hello.c

#include <emen/emen.h>
int main(int argc, char ** argv) {
printf("Hello Worldn");
}
#ifdef __cplusplus
extern "C" {
#endif
int EMEN_KEEPALIVE myFunction(int argc, char ** argv) {
printf("MyFunction Calledn");
}
#ifdef __cplusplus
}
#endif

如果想调用hello2.c中的myFunction方法,则需要将ccall方法从Moudule导出。使用下面的编译命令:

emcc -o hello2.html hello2.c -O3 -s
'EXTRA_EXPORTED_RUNTIME_METHODS=["ccall"]'
-s WASM=1 --shell-file html_template/shell_minimal.html

htmltemplate/shellminimal.html 指定为HTML模板。
-s 'EXTRAEXPORTEDRUNTIME_METHODS=["ccall"]' 从Module中导出 ccall

将 ccall 方法导出之后,就可以使用 Module.ccall来调用C++中的函数了。

var result = Module.ccall(
'funcName', // 函数名
'number', // 返回类型
['number'], // 参数类型
[42]); // 参数

更直观的例子

上面的例子中,编译后即可直接运行。但是生成的代码体积较大,不容易看懂具体做了什么。因此下面提供一个更直观的例子。

math.c

int add (int x, int y) {
return x + y;
}
int square (int x) {
return x * x;
}

编译:

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

-s SIDE_MODULE=1 直接由C生成wasm文件

目前只有一种方式能调用 wasm 里的提供接口,那就是:用 java !

编写加载函数(loader)

function loadWebAssembly (path) {
return fetch(path) // 加载文件
.then(res => res.arrayBuffer()) // 转成 ArrayBuffer
.then(WebAssembly.instantiate) // 编译 + 实例化
.then(mod => mod.instance) // 提取生成都模块
}

完成了上边的操作,就可以直接使用 loadWebAssembly 这个方法加载 wasm 文件了,它相当于是一个 wasm-loader ;返回值是一个 Promise.

loadWebAssembly('path/to/math.wasm')
.then(instance => {
const { add, square } = instance.exports
// ...
})

更完善的loader

function loadWebAssembly(filename, imports = {}) {
return fetch(filename)
.then(response => response.arrayBuffer())
.then(buffer => WebAssembly.compile(buffer))
//WebAssembly.compile 可以用来编译 wasm 的二进制源码,
//它接受 BufferSource 格式的参数,返回一个 Promise。
.then(module => {
imports.env = imports.env || {};
// 开辟内存空间 && 创建变量映射表
Object.assign(imports.env, {
memoryBase: 0,
tableBase: 0,
memory: new WebAssembly.Memory({ initial: 256, maximum: 256 }),
table: new WebAssembly.Table({ initial: 0, maximum: 0,
element: 'anyfunc' })
})
// 创建 WebAssembly 实例
return new WebAssembly.instantiate(module, imports)
})
}

ArrayBuffer 做了两件事情,一件是做 WebAssembly 的内存,另外一件是做 Java 的对象。

它使 JS 和 WebAssembly 之间传递内容更方便。
使内存管理更安全。

这个 loadWebAssembly 函数还接受第二个参数,表示要传递给 wasm 的变量,在初始化 WebAssembly 实例的时候,可以把一些接口传递给 wasm 代码。

asm.js

asm.js 是 java 的子集,是一种语法。用了很多底层语法来标注数据类型,目的是提高 java 的运行效率,本身就是作为 C/C++ 编译的目标设计的(不是给人写的)。 WebAssembly 借鉴了这个思路,做的更彻底一些,直接跳过 java ,设计了一套新的平台指令。

目前只有 asm.js 才能转成 wasm,普通 java 是不行的。虽然 Emen 能生成 asm.js 和 wasm ,但是却不能把 asm.js 转成 wasm 。想要把 asm.js 编译成 WebAssembly,就要用到他们官方提供的 Binaryen 和 WABT (WebAssembly Binary Toolkit) 工具。

Binaryen WABT
math.js --------> math.wast -------> math.wasm

Rust编译为webAssembly

1.安装Rustup

Rustup是一个命令行应用,能够下载并在不同版本的Rust工具链中进行切换

brew install cargo
curl https://sh.rustup.rs -sSf | sh
source $HOME/.cargo/env
source ~/.bash_profile
rustup target add wasm32-unknown-unknown --toolchain nightly
cargo install --git https://github.com/alexcrichton/wasm-gc
//减小wasm的size

cargo可以将整个工程编译为wasm,首先使用cargo创建工程:

cargonewproject

下一步,把下面的代码加到 Cargo.toml 中

[lib]
path = "src/lib.rs"
crate-type = ["cdylib"]

2.demo:https://github.com/jakedeichert/wasm-astar

编译:

cargo+nightly build--target wasm32-unknown-unknown--release

编译出来的wasm大小为82Kb,使用wasm-gc压缩 small-wasm_astar.wasm 的大小为 67Kb

wasm-gc wasm_astar.wasm small-wasm_astar.wasm

为什么WebAssembly更快

JS 引擎在图中各个部分所花的时间取决于页面所用的 Java 代码。图表中的比例并不代表真实情况下的确切比例情况。

Parse: 把源代码变成解释器可以运行的代码所花的时间;
Compiling + optimizing: 基线编译器和优化编译器花的时间;
Re-optimize: 当 JIT 发现优化假设错误,丢弃优化代码所花的时间。
Execut:执行代码的时间
Garbage collection: 垃圾回收,清理内存的时间

文件获取:

WebAssembly比JS的压缩了更高,所以文件获取更快。

解析:

到达浏览器时,JS源代码被解析成了抽象语法树,浏览器采用懒加载的方式进行,只解析真正需要的部分,,而对于浏览器暂时不需要的函数只保留它的桩,解析过后 AST (抽象语法树)就变成了中间代码(叫做字节码),提供给 JS 引擎编译。

而WebAssembly不需要这种转换,因为它本身就是中间代码,它要做的只是解码并且检查确认代码没有错误即可。

编译和优化

Java 是在代码的执行阶段编译的。因为它是弱类型语言,当变量类型发生变化时,同样的代码会被编译成不同版本。

不同浏览器处理 WebAssembly 的编译过程也不同。不论哪种方式,WebAssembly 都更贴近机器码,所以它更快.

在编译优化代码之前,它不需要提前运行代码以知道变量都是什么类型。
编译器不需要对同样的代码做不同版本的编译。
很多优化在 LLVM 阶段就已经做完了,所以在编译和优化的时候没有太多的优化需要做。

重优化

JS的代码由于类型的不确定性,有些情况下,JIT会返回进行 “抛弃优化代码<->重优化”过程。

而WebAssembly中,类型都是确定了的,因为没有重优化阶段。

执行

WebAssembly 就是为了编译器而设计的,开发人员不直接对其进行编程,这样就使得 WebAssembly 专注于提供更加理想的指令给机器。

执行效率方面,不同的代码功能有不同的效果,一般来讲执行效率会提高 10% - 800%。

垃圾回收

WebAssembly不支持垃圾回收,内存操作需要手动控制,因此WebAssembly没有垃圾回收。

应用

WebAssembly 更适合用于写模块,承接各种复杂的计算,如图像处理、3D运算、语音识别、视音频编码解码这种工作,主体程序还是要用 java 来写的。

未来功能

直接操作DOM
支持多数据(SIMD):SIMD的使用可以获取大的数据结构,例如不同数目的向量,并且同时将相同的指令应用于不同的部分。这样,它可以大大加快各种复杂计算的游戏或VR的运行速度。
ES6模块集成:浏览器目前正在添加对使用标签加载Java模块的支持。 添加此功能后,即使URL指向WebAssembly模块,

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容

  • 转自:https://fed.renren.com/2017/05/21/webassembly/ Webasse...
    core1988阅读 2,686评论 0 1
  • Webassembly(WASM)和CSS的Grid布局一样都是一个新东西,Chrome从57开始支持。在讲was...
    极乐君阅读 1,728评论 1 4
  • 谁曾料想 平凡无奇的人生走到半道 命运偏偏为我翻开了另一张牌 成为一名写作者 可能这就是冥冥之中的安排吧 初进有书...
    卡兰诺阅读 1,241评论 13 20
  • 想成为一名教师,不只是说说而已,我讲真! 作为一名大三的学生,我曾虚度光阴,也曾勇敢尝试;执着于未来,却又不知何去...
    零一春风数声阅读 305评论 0 1