亿点点JS性能历史
1995年,javaScript 诞生并迅速发展。2008年,浏览器JITs即时编译器的出现,让js的执行速度提升了10倍。网站上可以加入的设计越来越多。随着越来越多的框架和工具的诞生,前端项目的上限越来越高。
那么如果我想在前端实现视频剪辑、图片处理、VR等计算量比较大的操作可行吗?答案当然是可行的,但是以当前js的计算速度,在执行复杂的计算任务时,你的客户还没等到你加载计算完成可能已经离开了当前页面了。那3D游戏呢?行是行,前提是客户可以接受ppt般的游戏体验。
而WebAssembly(wasm)的出现就是为了解决这方面的问题的。
更快的计算-WebAssembly
根据MDN的定义,WebAssembly是一种新的编码方式,可以在现代的网络浏览器中运行 - 它是一种低级的类汇编语言,具有紧凑的二进制格式,可以接近原生的性能运行,并为诸如C / C ++等语言提供一个编译目标,以便它们可以在Web上运行。它也被设计为可以与JavaScript共存,允许两者一起工作。
我们知道,高级语言由于靠近人类语言所以学习的成本更低,但是运行效率并不高,高级语言经过编译等一系列操作转换成低级语言——机器可以理解的二进制语言。当一门语言越靠近机器语言,中间各种转换操作可以省略而变得更快执行。你可以这样理解,先有机器语言,但为了方便人们开发使用,于是有了高级语言,为了让机器理解高级语言,高级语言需要进行转换。
WebAssembly是一种低级的类汇编语言,二进制格式和靠近机器码的实现使它在执行相同的操作时比 javaScript 要快得多。一些编译工具为 C / C++ 提供转换成wasm,Web可以运行这类文件,它们通常以.wasm结尾,看到这里,有些小伙伴已经有点眼熟了,在一些网站打开调试工具,我们可以在network栏里发现这类文件的存在,但当时笔者并不知道它们是什么 [doge]。
就当前我们公司的需求来说,用户传图进行人脸识别是我们其中一块重要的功能,人脸识别这类功能可以使用原本使用 C++ 开发的人脸识别库,只要这个库编译成WebAssembly开放使用。这样我们就把人脸识别的工作交给了wasm进行,甚至在一些不涉及dom的场景,我们可以通过worker计算而不占用主线程。如此一来,wasm结合js可以帮助我们更快的实现一些消耗性能的操作。
综上所述,因为省略了许多从高级语言转换成低级语言的操作,同时不需要对wasm不需要额外的类型优化与垃圾回收,所以WebAssembly可以的更快执行任务。
wasm使用指南🧭
导入
WebAssembly尚未与<script type='module'>
或ES2015 import
语句集成,因此没有路径让浏览器通过导入来获取模块。
这项技术正在研究推进中,未来WebAssembly模块将可以使用
<script type='module'>
加载,这意味着JavaScript将能够像ES2015模块一样容易地获取,编译和导入WebAssembly模块。
目前我们可以通过使用fetch
来提取网络资源。
WebAssembly.instantiateStreaming(fetch('simple.wasm'), importObject)
.then(results => {
// Do something with the results!
});
需要注意的是,instantiateStreaming
方法在safari上并不支持,为了兼容更多的浏览器,我们可以更改使用instantiate
,该方法唯一的区别就是需要执行一个额外的步骤,将获取的字节码转换为ArrayBuffer
。
fetch('module.wasm').then(response =>
response.arrayBuffer()
).then(bytes =>
WebAssembly.instantiate(bytes, importObject)
).then(results => {
// Do something with the results!
});
instantiate
返回wasm的Module与实例(Instance
),Module与实例分别是什么东西?接下来认识几个wasm API中的几个关键概念。
相关概念
- Module:表示一个 WebAssembly 二进制文件,该二进制文件已由浏览器编译为可执行的机器代码。 Module 是无状态的,因此,就像
Blob
一样,可以在 Windows 和 worker 之间(通过postMessage()
)共享。模块声明导入和导出就像 ES2015module 一样。 - importObject:包含一些想要导入到新创建
Instance
中值的对象,例如Table
对象或Memory
对象。 - Memory:一个
Memory
对象,该对象的buffer
属性是可调整大小的ArrayBuffer
,其内存储的是 WebAssembly实例可访问内存里的原始字节码。 - Table:构造函数根据给定的大小和元素类型(目前只有函数元素类型
"anyfunc"
)创建一个Table
对象。具有get
、grow
和set
三种方法,set
用于设置对象里给定索引处的元素设置给定函数,get
则用于获取,grow
则用于增长表长度。 - 实例:一个可执行实例对象,其与运行时使用的所有状态配对,包括
Memory
,Table
和一组导入的值(importObject
), 包含所有的WebAssembly导出函数。
Module 与 实例
Module
可以在 Windows 和 worker 共享,这样我们就能把合适的工作交给worker,worker进行实例化并返回结果给 Windows。
// index.js
var worker = new Worker("wasm_worker.js");
WebAssembly.compileStreaming(fetch('simple.wasm'))
.then(mod =>
worker.postMessage(mod)
);
compileStreaming
与instantiateStreaming
类似,区别在于compileStreaming
并不返回实例,仅仅做了编译,返回一个 Module 。接下来,我们把 Module 传递给 worker 。
compileStreaming
同样存在Safari兼容问题,可以使用compile
替代,同instantiate
方法,该方法需要一个额外的步骤,将获取的字节码转换为ArrayBuffer
。
simple.wasm:
(module
(func $imports.imported_func (;0;) (import "imports" "imported_func") (param i32))
(func $exported_func (;1;) (export "exported_func")
i32.const 42
call $imports.imported_func
)
)
wasm_worker.js:
var importObject = {
imports: {
imported_func: function(arg) {
console.log(arg);
}
}
};
onmessage = function(e) {
console.log('module received from main thread');
var mod = e.data;
WebAssembly.instantiate(mod, importObject).then(function(instance) {
instance.exports.exported_func();
});
var exports = WebAssembly.Module.exports(mod);
console.log(exports[0]);
};
worker 接收到实例后,使用instantiate
进行实例化,得到实例。exported_func
是 wasm 导出的其中一个函数方法,我们只需像调用 js 函数一样调用 wasm 导出的函数即可。除此之外我们可以通过静态方法WebAssembly.Module.exports
获取wasm导出的对象数组。在上面这个例子中,export 导出的对象数组如下
[
{
"name": "[exported_func](url)",
"kind": "function"
}
]
WebAssembly.Module.imports
可以获取wasm接受的导入对象数组,在上面这个例子中,import导入对象数组如下
[
{
"module": "imports",
"name": "imported_func",
"kind": "function"
}
]
通过观察导入对象数组,我们可以编写包含 wasm 可执行的 javaScript 函数对象importObject
并传入实例化函数。在上面这个例子中,wasm 的exported_func
方法调用了importObject
传入的imported_func
方法,打印了 wasm 中的42常量。
尝试调试Module与worker结合使用实例以更好地理解以上例子。
内存
在 wasm 与 javaScript 的协作中,我们可以使用WebAssembly.Memory创建内存实例完成两者的信息传递。
var memory = new WebAssembly.Memory({initial:10, maximum:100});
以上代码创建了一个初始为10,最大可以为100的用于 wasm 存储数据的内存空间。memory.buffer
返回原始二进制数据缓冲区ArrayBuffer,如果我们需要对memory进行赋值,根据mdn的定义,我们无法直接操作ArrayBuffer的内容,但可以创建其中一种类型化数组对象或使用DataView。
var i32 = new Uint32Array(memory.buffer);
for (var i = 0; i < 10; i++) {
i32[i] = i;
}
通过这种方式,我们把0 - 9赋值到32位无符号整数类型数组中。
接下来给出一个提供把数组中的值进行加法计算的wasm文件:
(module
(memory $js.mem (;0;) (import "js" "mem") 1)
(func $accumulate (;0;) (export "accumulate") (param $var0 i32) (param $var1 i32) (result i32)
(local $var2 i32) (local $var3 i32)
local.get $var0
local.get $var1
i32.const 4
i32.mul
i32.add
local.set $var2
block $label0
loop $label1
local.get $var0
local.get $var2
i32.eq
br_if $label0
local.get $var3
local.get $var0
i32.load
i32.add
local.set $var3
local.get $var0
i32.const 4
i32.add
local.set $var0
br $label1
end $label1
end $label0
local.get $var3
)
)
在WebAssembly的低级内存模型中,内存表示为连续的无类型字节范围,称为线性内存。这里简单介绍下这种内存下的计算方式,我们选取其中一段wasm进行简单的解释
local.get $var0
local.get $var1
i32.const 4
i32.mul
i32.add
local.set $var2
- $var0推入栈顶
- $var1推入栈顶
- 4推入栈顶
- 推出栈顶两次,将两次获得的数据进行相乘,结果推入栈顶
- 推出栈顶两次,将两次获得的数据进行相加,结果推入栈顶
在上述wasm文件中,$accumulate
有两个参数,可以理解为相加的数组起点和数组的终点,这个函数中的本地变量$var2
计算为数组的边界值,$var0
则是在循环中增长的值,在每次循环中增加4,当两者相等时,循环结束。
总之,我们可以理解为每次循环中,每次按4个字节的增长偏移顺序读取了Memory
——即按顺序读取了Uint32Array
中的值进行了相加操作,并最终赋值给$var3
并输出了结果,即Uint32Array
中0-9位的值之和。
完整的js调用如下:
var memory = new WebAssembly.Memory({initial:10, maximum:100});
WebAssembly.instantiateStreaming(fetch('memory.wasm'), { js: { mem: memory } })
.then(obj => {
var i32 = new Uint32Array(memory.buffer);
for (var i = 0; i < 10; i++) {
i32[i] = i;
}
var sum = obj.instance.exports.accumulate(0, 10);
console.log(sum);
});
尝试调试内存实例以更好地理解以上例子。
Table
以下wasm,展示wasm导出了一个table,包含两个元素: $thirteen
和$fourtytwo
方法。
(module
(func $thirteen (result i32) (i32.const 13))
(func $fourtytwo (result i32) (i32.const 42))
(table (export "tbl") anyfunc (elem $thirteen $fourtytwo))
)
接下来是js的部分(省略了导入步骤):
var tbl = results.instance.exports.tbl;
console.log(tbl.get(0)()); // 13
console.log(tbl.get(1)()); // 42
请注意,通过Table.prototype.get()调用来检索每个函数引用,然后在最后添加一个额外的括号以调用该函数。
尝试调试table实例以更好地理解以上例子。
总结
- WebAssembly提供了一种以近乎本机的速度在网络上运行,与 javaScript 赋予了Web极大的性能提升
- 可以使用再视频编辑,图像编辑,3D游戏,VR等需要极大性能提升的场景
- WebAssembly并非使用纯手工编写,而是旨在成为C,C ++,Rust等源语言的有效编译目标
- 通过wasm实例/memory/table交换信息,达到运行wasm中的相关代码并得到结果的目的。