阅读 311

前端展望-WebAssembly(wasm)技术入门

亿点点JS性能历史

1995年,javaScript 诞生并迅速发展。2008年,浏览器JITs即时编译器的出现,让js的执行速度提升了10倍。网站上可以加入的设计越来越多。随着越来越多的框架和工具的诞生,前端项目的上限越来越高。

编组 6.png

那么如果我想在前端实现视频剪辑、图片处理、VR等计算量比较大的操作可行吗?答案当然是可行的,但是以当前js的计算速度,在执行复杂的计算任务时,你的客户还没等到你加载计算完成可能已经离开了当前页面了。那3D游戏呢?行是行,前提是客户可以接受ppt般的游戏体验。

而WebAssembly(wasm)的出现就是为了解决这方面的问题的。

更快的计算-WebAssembly

根据MDN的定义,WebAssembly是一种新的编码方式,可以在现代的网络浏览器中运行 - 它是一种低级的类汇编语言,具有紧凑的二进制格式,可以接近原生的性能运行,并为诸如C / C ++等语言提供一个编译目标,以便它们可以在Web上运行。它也被设计为可以与JavaScript共存,允许两者一起工作。

我们知道,高级语言由于靠近人类语言所以学习的成本更低,但是运行效率并不高,高级语言经过编译等一系列操作转换成低级语言——机器可以理解的二进制语言。当一门语言越靠近机器语言,中间各种转换操作可以省略而变得更快执行。你可以这样理解,先有机器语言,但为了方便人们开发使用,于是有了高级语言,为了让机器理解高级语言,高级语言需要进行转换。

WebAssembly是一种低级的类汇编语言,二进制格式和靠近机器码的实现使它在执行相同的操作时比 javaScript 要快得多。一些编译工具为 C / C++ 提供转换成wasm,Web可以运行这类文件,它们通常以.wasm结尾,看到这里,有些小伙伴已经有点眼熟了,在一些网站打开调试工具,我们可以在network栏里发现这类文件的存在,但当时笔者并不知道它们是什么 [doge]。

infografica-Web-Assembly-eng.jpeg

就当前我们公司的需求来说,用户传图进行人脸识别是我们其中一块重要的功能,人脸识别这类功能可以使用原本使用 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 一样。

  • Memory:可调整大小的 ArrayBuffer ,包含由 WebAssembly 的低级内存访问指令读取和写入的线性字节数组。

  • Table:可调整大小的类型化引用数组(例如,函数)。

  • 实例:一个模块,其与运行时使用的所有状态配对,包括MemoryTable和一组导入的值(importObject)。

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) ); 复制代码

compileStreaminginstantiateStreaming类似,区别在于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 复制代码

  1. $var0推入栈顶

  2. $var1推入栈顶

  3. 4推入栈顶

  4. 推出栈顶两次,将两次获得的数据进行相,结果推入栈顶

  5. 推出栈顶两次,将两次获得的数据进行相,结果推入栈顶

image.png

在上述wasm文件中,$accumulate有两个参数,可以理解为相加的数组起点和数组的终点,这个函数中的本地变量$var2计算为数组的边界值,在每次循环中增加4。

这里笔者猜测对与wasm来说,传入的数组以8位二进制为一组,每次循环增加4,则刚好为Uint32Array中一个数据的长度。

总之,我们可以理解为每次循环中,按顺序读取了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中的相关代码并得到结果的目的。


作者:只要我E得够快
链接:https://juejin.cn/post/6963175242613293086


文章分类
后端
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐