WebAssembly使用方法研究
作者:郝同学1208
序
之前在浏览前端技术的时候留意到了webAssembly这项技术,看到它是类似于在前端调用后端语言的源码或者说功能,再加上笔者浅学过C++和java,故在此做一些调研和尝试
什么是WebAssembly
WebAssembly以下简称WASM,通过将传统意义上的后端语言(C、C++、Java、Rust等)编译成字节码,.wasm格式文件,在浏览器上调用解释器编译成机器码才能运行,因此WASM并不能真正达到汇编语言级别的性能,与Java比较像,都是编译成中间字节码,然后交由解释器工作(在Java中则是JVM)。
2015年4月,WebAssembly Community Group 成立;
2015年6月,WebAssembly第一次以WCG的官方名义向外界公布;
2016年8月,WebAssembly开始进入了漫长的“Browser Preview”阶段;
2017年2月,WebAssembly官方LOGO在Github上的众多讨论中被最终确定;同年同月,一个历史性的阶段,四大浏览器(FireFox、Chrome、Edge、WebKit)在WebAssembly的MVP(最小可用版本)标准实现上达成共识,这意味着WebAssembly在其MVP标准上的“Brower Preview”阶段已经结束;
2017年8月,W3C WebAssembly Working Group 成立,意味着WebAssembly正式成为W3C众多技术标准中的一员。
2019年12月,WebAssembly成为万维网联盟(W3C)的推荐标准,与HTML,CSS和JavaScript一起成为Web的第四种语言。
使用方法
从.wasm源文件到实例化的对象主要有三个步骤,加载->编译->实例化->调用。
加载:读取.wasm字节码到本地中,一般是通过请求从网络中取得。
编译:在Worker线程进行,编译成平台相关的代码。
实例化:将宿主环境的一些对象、方法导入到wasm模块中,比如导入操作dom的方法。
调用:通过上一步已经实例化的对象,来调用wasm模块中的方法。主要有两种类型的API,一种是js提供的api,另一种是Web提供的api,Web提供的api支持流式编译实例化。
js的方法,WebAssembly.instantiate(bufferSource,importObject),可以完成编译和实例化。
bufferSource是含有效Wasm模块二进制字节码的ArrayBuffer或TypedArray对象。importObject是要导入到Wasm模块中的对象。方法在调用后返回一个Promise对象,resolve后返回一个对象,该对象包含编译好的module和已经实例化的instance,模块导出的方法可以通过instance对象进行调用。
web的方法,WebAssembly.instantiateStreaming(source,importObject)。不同之处在于第一个参数,这里的source指的是尚未Resolve的Response对象(window.fetch调用后会返回该对象),好处就是可以边读取.wasm字节流,边进行编译。其他参数和返回值和js的api均一致。
jsAPI尝试
先简单的尝试一下,我们直接构造一个wasm模块的TypedArray对象,该模块包含了一个add方法,然后调用WebAssembly.instantiate进行编译和实例化。对应的C++代码。
#include <emscripten.h> extern "C" { EMSCRIPTEN_KEEPALIVE int add(int a, int b) { return a + b; } } 对应的.wasm字节码: 00 61 73 6D 01 00 00 00 01 17 05 60 00 01 7F 60 00 00 60 01 7F 00 60 01 7F 01 7F 60 02 7F 7F 01 7F 03 07 06 01 04 00 02 03 00 04 05 01 70 01 02 02 05 06 01 01 80 02 80 02 06 0F 02 7F 01 41 90 88 C0 02 0B 7F 00 41 84 08 0B 07 82 01 09 06 6D 65 6D 6F 72 79 02 00 19 5F 5F 69 6E 64 69 72 65 63 74 5F 66 75 6E 63 74 69 6F 6E 5F 74 61 62 6C 65 01 00 03 61 64 64 00 01 0B 5F 69 6E 69 74 69 61 6C 69 7A 65 00 00 10 5F 5F 65 72 72 6E 6F 5F 6C 6F 63 61 74 69 6F 6E 00 05 09 73 74 61 63 6B 53 61 76 65 00 02 0C 73 74 61 63 6B 52 65 73 74 6F 72 65 00 03 0A 73 74 61 63 6B 41 6C 6C 6F 63 00 04 0A 5F 5F 64 61 74 61 5F 65 6E 64 03 01 09 07 01 00 41 01 0B 01 00 0A 30 06 03 00 01 0B 07 00 20 00 20 01 6A 0B 04 00 23 00 0B 06 00 20 00 24 00 0B 10 00 23 00 20 00 6B 41 70 71 22 00 24 00 20 00 0B 05 00 41 80 08 0B 然后直接在控制台输入下边的代码: WebAssembly.instantiate(new Uint8Array(` 00 61 73 6D 01 00 00 00 01 17 05 60 00 01 7F 60 00 00 60 01 7F 00 60 01 7F 01 7F 60 02 7F 7F 01 7F 03 07 06 01 04 00 02 03 00 04 05 01 70 01 02 02 05 06 01 01 80 02 80 02 06 0F 02 7F 01 41 90 88 C0 02 0B 7F 00 41 84 08 0B 07 82 01 09 06 6D 65 6D 6F 72 79 02 00 19 5F 5F 69 6E 64 69 72 65 63 74 5F 66 75 6E 63 74 69 6F 6E 5F 74 61 62 6C 65 01 00 03 61 64 64 00 01 0B 5F 69 6E 69 74 69 61 6C 69 7A 65 00 00 10 5F 5F 65 72 72 6E 6F 5F 6C 6F 63 61 74 69 6F 6E 00 05 09 73 74 61 63 6B 53 61 76 65 00 02 0C 73 74 61 63 6B 52 65 73 74 6F 72 65 00 03 0A 73 74 61 63 6B 41 6C 6C 6F 63 00 04 0A 5F 5F 64 61 74 61 5F 65 6E 64 03 01 09 07 01 00 41 01 0B 01 00 0A 30 06 03 00 01 0B 07 00 20 00 20 01 6A 0B 04 00 23 00 0B 06 00 20 00 24 00 0B 10 00 23 00 20 00 6B 41 70 71 22 00 24 00 20 00 0B 05 00 41 80 08 0B`.trim().split(/[\s\r\n]+/g).map(str => parseInt(str, 16)) )).then(({instance}) => { const { add } = instance.exports console.log('2 + 4 =', add(2, 4)) }) 输出2 + 4 = 6
WebAPI尝试
我们再尝试一下流式编译。直接使用之前的斐波纳契数字的fibonacci.wasm模块。首先我们需要提供一个简单的HTTP服务,用来返回.wasm文件。新建一个node.js文件。
const http = require('http'); const url = require('url'); const fs = require('fs'); const path = require('path'); const PORT = 8888; // 服务器监听的端口号; const mime = { "html": "text/html;charset=UTF-8", "wasm": "application/wasm" //当遇到对".wasm"格式文件的请求时,返回特定的MIME头; }; http.createServer((req, res) => { let realPath = path.join(__dirname, `.${url.parse(req.url).pathname}`); //检查所访问文件是否存在,且是否可读; fs.access(realPath, fs.constants.R_OK, err => { if (err) { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end(); } else { fs.readFile(realPath, "binary", (err, file) => { if (err) { //文件读取失败时返回500; res.writeHead(500, { 'Content-Type': 'text/plain' }); res.end(); } else { //根据请求的文件返回相应的文件内容; let ext = path.extname(realPath); ext = ext ? ext.slice(1) : 'unknown'; let contentType = mime[ext] || "text/plain"; res.writeHead(200, { 'Content-Type': contentType }); res.write(file, "binary"); res.end(); } }); } }); }).listen(PORT); console.log("Server is runing at port: " + PORT + ".");
然后来编写我们的js部分,讲到斐波那契数字,我们顺便做一个性能的测试,来比较一下使用wasm的方式和原生js的求解速度。
function fibonacciJS(n) { if (n < 2) { return 1; } return fibonacciJS(n - 1) + fibonacciJS(n - 2); } const response = fetch("fibonacci.wasm"); const num = [5, 15, 25, 35, 45]; WebAssembly.instantiateStreaming(response).then( ({ instance }) => { let { fibonacci } = instance.exports; for(let n of num) { console.log(`斐波纳切数字: ${n},运行 10 次`) let cTime = 0; let jsTime = 0; for(let time = 0; time < 10; time++) { let start = performance.now(); fibonacci(n) cTime += (performance.now() - start) start = performance.now(); fibonacciJS(n) jsTime += (performance.now() - start) } console.log(`wasm 模块平均调用时间:${cTime / 10}ms`) console.log(`js 模块平均调用时间:${jsTime / 10}ms`) } } )
然后执行node node.js开启http服务,接着在浏览器中打开http://localhost:8888/index.html,控制台中输出如下:
斐波纳切数字: 5,运行 10 次
index.html:34 wasm 模块平均调用时间:0.001499993959441781ms
index.html:35 js 模块平均调用时间:0.005500001134350896ms
index.html:22 斐波纳切数字: 15,运行 10 次
index.html:34 wasm 模块平均调用时间:0.005999993300065398ms
index.html:35 js 模块平均调用时间:0.15650001005269587ms
index.html:22 斐波纳切数字: 25,运行 10 次
index.html:34 wasm 模块平均调用时间:0.6239999900572002ms
index.html:35 js 模块平均调用时间:1.1620000121183693ms
index.html:22 斐波纳切数字: 35,运行 10 次
index.html:34 wasm 模块平均调用时间:70.59700000681914ms
index.html:35 js 模块平均调用时间:126.21099999523722ms
index.html:22 斐波纳切数字: 45,运行 10 次
index.html:34 wasm 模块平均调用时间:8129.7520000021905ms
index.html:35 js 模块平均调用时间:16918.658500007587ms
可以看到wasm很明显的提高了运行速度,运行时间稳定在js的一半,当规模达到45的时候,wasm的运行时间比js少了整整8秒。
这里也可以看出,如果对于计算密集型的应用,wasm可以大展身手了,因此WASM适合运用于游戏、视频处理、AR等方面。
不止于WebWasm
除了应用在浏览器中,也可以应用到out-of-web环境中。通过WASI(WebAssembly System Interface,Wasm操作系统接口)标准,Wasm可以直接与操作系统打交道。通过已经在各种环境实现了WASI标准的虚拟机,我们就可以将wasm用在嵌入式、IOT物联网以及甚至云,AI和区块链等特殊的领域和场景中。
有了WASI标准,文章最开始介绍的当前应用的架构在未来可能会发生质的改变。
上边架构的最大问题就是各个操作系统不能兼容,同一个app需要采用不同的语言在不同平台下各实现一次。比如一款A应用,如果想实现跨平台的话,我们需要用java完成在安卓上的开发,用Objective-C实现iOS上的开发,用C#实现PC端的开发...但如果有了wasm,我们只需要选择任意一门语言,然后编译成wasm,就可以分发到各个平台上了。
总结
目前来讲WASM在浏览器性能上和JS能打个55开,互有胜负,WASM有他擅长的地方,JS有JS优势的地方,我认为WASM并不会替代JS/TS,WASM的统一和整合,正如下图
将所有的开发语言和所有的运行环境连接起来,只不过因为web天生的多平台性,目前在web端应用比较成熟,而这也确实在某些业务上能够让前端直接去调用C++的相关代码。
但是就目前来讲,你指望一个前端工程师为了性能优化的原因,去学习C++然后再编写C++代码编译成.wasm二进制文件再放到浏览器环境上运行?不可能的,所以目前WASM的应用主要还是在JS没有相应合适的三方库的情况下,去借用C++等后端语言的三方库,比如视频处理中的ffmpeg;或者将一部分后端的逻辑处理放到前端,减少服务压力,不过这也是很没必要。
我认为,WASM目前阶段已经具备了很多成熟的应用,而且未来可期,作为未来的技术方向有必要了解一下。
以上就是WebAssembly使用方法研究的详细内容,更多关于WebAssembly使用方法的资料请关注脚本之家其它相关文章!