文章序
之前在浏览前端技术的时候留意到了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目前阶段已经具备了很多成熟的应用,而且未来可期,作为未来的技术方向有必要了解一下。
网友评论