首先,按照惯例,科普下啥是 webssembly
.wasm
未后缀的二进制格式;那么,这玩意是为啥而诞生的呢?
那就得从1995年说起了,那一年,我刚学会走路,Javascript 诞生了,并且从此一发不可收拾,推动了web的迅速发展。如果把WEB看作是一辆车,那么 Javascript 就是车子的发动机,随着人们对车子的要求越来越高,Javascript 这发动机的毛病就逐渐体现出来,主要有两点:
为了解决这问题,Google 在 2009 年在 V8 中引入了 JIT 技术(Just in Time Compiling),通过各种编译优化直接将 JavaScript 编译成运行在 CPU 上的机器码。JavaScript 的性能提升了 20 - 40 倍。
20 - 40倍!是不是觉着这时候车子都可以飞起来了,然而,实际并不是这样,因为,JavaScript 原来的执行效率实在太低了。
JIT 技术也有自身的缺陷,V8 会通过类型推断来减少对弱类型变量的拆装箱。但是 JavaScript 是动态类型的,如果我们在编写代码的时候改变变量的类型。这会导致 JIT 的重编译,有时候 V8 的性能提升,还没重编译的开销大。
那该咋办呢?这时候,你可能会说,那我在写代码的时候不改变变量的类型不就好了吗!是的,这个思路不错,并且,有人直接付出行动了,Microsoft 开发了 TypeScript, 通过为 JS 加入静态类型检查来改进 JS 松散的语法,提升代码健壮性。Google 则开发了的 Dart,为浏览器引入新的虚拟机去直接运行 Dart 程序以提升性能;Mozilla 更是研究出了 JavaScript 的子集 asm.js,JavaScript Engine 针对 asm.js 进行性能优化。
但是,好像没有解决根本问题,TypeScript 只是解决了 JS 语法松散的问题,最后还是需要编译成 JS 去运行,对性能并没有提升,Dart 没有主流浏览器支持,arm.js 语法太简单、条件过于苛刻,开发效率低。况且,3大巨头各玩各的,这怎么能行,违背了地球和平统一的原则!!
所以,咱们的主角,WebAssembly,诞生了!它即高效又安全,即开放又符合浏览器标准。WebAssembly 并不是一门编程语言,而是一份字节码标准,需要使用高级编程语言编译出字节码放到 WebAssembly 的虚拟机中运行(有点像 Java ),目前主流浏览器都已经支持 WebAssembly。
首先,我们要将高级语言通过编译器翻译成一种更低级的语言- 中间码(IR),再经过对应工具链转换,生成对应的.wasm
文件
wasm
目前较为常见的是将 C/C++ 代码或者是 Rust 代码转为 wasm 文件,那么,我们就分别来看看这两种方式是怎么操作的:
####…… Case 1: C to wasm
具体步骤如下:
Step 1: 环境安装
Step 2: Emscripten SDK安装
Step 3: 编写 C 代码
首先,我们来编写一段c代码,我们可以看到,我们先是生命了一个主函数,输出了一个hello word,下面又生命了一个自定义函数,也打印了一段文本:
wasm
由于Emscripten编译器生成的代码只会只会调用main()主函数,其他函数会被认为是无效代码而消除,因此需要从emscripten.h库中引入EMSCRIPTEN_KEEPALIVE修饰该函数,告诉编译器该函数需要被导出。
Step 4: 执行命令生成 wasm 文件
emcc -o index.html index.c -O3 -s WASM=1 --shell-file html_template/template.html -s NO_EXIT_RUNTIME=1 -s "EXTRA_EXPORTED_RUNTIME_METHODS=['ccall']"
.wasm
文件--shell-file
: 指定 html 模版文件wasm
Step 5: 调用 C 中的方法
在生成的index.html文件中,添加如下代码,执行anywhere(一个node本地服务插件 -- npm install anywhere -g
),启动一个本地服务,打开html页面,点击页面中添加的按钮,便可在控制台中看到C中函数被调用了:
<button>点我点我</button>
<script type="text/javascript">
document.querySelector('myButton').addEventListener('click', function() {
var result = Module.ccall(
'myFunction', // C代码中的方法名
null, // 函数返回值类型
null, // 参数类型
null // 参数
)
})
</script>
Step 1: 环境安装
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
export PATH=“$PATH:$HOME/.cargo/env"
cargo install wasm-pack
Step 2: 初始化一个Rust项目
首先,我们通过命令脚本创建一个rust工程:
cargo new --lib my-wasm
这个工程的结构与我们webpack前端工程很像,lib.rs就是主要逻辑代码存放的地方,cargo.toml则是类似于我们package.json一样的配置文件;
wasm
Step 3: 修改Rust代码
同样,我们直接开始修改lib.rs文件的内容,使用wasm_bindgen绑定修饰方法,该方法才会被编译器打包输出,extern 内部包含的代码,就是在定义一些js端传递过来的代码,以便在rs代码中调用。
wasm
Step 4: 修改配置文件
接下我们修改配置文件,添加上相应的依赖
wasm
Step 5: 编译生成产物
最后一步就是执行命令,将rust代码编译生成wasm文件:
wasm-pack build --target bundler
这个命令有两种后缀
这命令主要干了一下这些事:
wasm_bindgen
,生成一个 js 文件,将 webassembly 文件引入到一个 NPM 可以识别的模块中wasm
前面说的是如何生成 wasm 文件,接下来就是怎么样去使用 wasm 文件,其实,我们拿到的文件内容后,需要将它转换成 arrayBuffer,再通过原声的 Webassembly.instance
方法接收,最后返回的才是我们能阅读的代码
其实很简单,如下图:
wasm
那么,在react工程中,也那么简单吗?
其实差不多,我们只需要在webpack添加相应的loader
即可:
yarn add wasm-loader && yarn add node-fetch
wasm
其实,在实例化WebAssembly模块时,需要一个内存对象。您可以创建一个新的WebAssembly.Memory并将该对象传递进来。否则,将自动创建一个内存对象并将其附加到实例:
wasm
wasm
因为这只是一个JavaScript对象,所以这意味着JavaScript也可以在该内存的字节中挖掘数据。因此,以这种方式,WebAssembly和JavaScript可以共享内存并来回传递值:
wasm
WebAssembly可以在内存中放置一个字符串。它将编码为字节…然后将这些字节放入数组中
wasm
然后它将第一个索引(整数)返回给JavaScript。因此,JavaScript可以提取字节并使用它们
wasm
那么关于共享内存,有以下两个需要关注的问题:
我们再来看业界中两个使用WebAssembly成功的较为典型的案例
wasm
这个网站的主要功能是做一些图像的处理,我们可以在上面上传一张图片,然后可以离线的去进行一些对图像的处理,比如说进行图片压缩、量化或者smozing这样的一些处理,我们可以看到,相对来说,我们的一些操作,页面中很快就能展现出处理后的效果
如果仔细去研究这个网站的实现,我们会发现它依赖了很多现有的成熟的库,编译成了wasm来使用的。比如说libimagequant、MozJPEG、webp,这些都是目前业界内对图像处理非常优秀的一些库。
wasm
ebay 网页端的扫码功能,在使用了wasm技术后,说是得到了50倍的性能提升,他们在实现这个barcode scanner的时候,也是选用了业界比较有名的ZBar这样一个库,将它编译成了wasm来使用;
当然,现在并不是所有的浏览器都支持wasm的,所以肯定需要一些备用方案或者说是兜底逻辑
而ebay他们的做法是采用了竞争的模式,同时起了3个线程,每个对应一个worker,第一个worker执行的是Zbar对应的wasm文件,第二个是他们自研的一个仓库转换成wasm,然后第三个worker执行的是原声的js,当扫码条形码时,三个worker同时运行,哪一个worker最先返回结果,就是用这个结果。
wasm
以这种形式,来保证不管是在哪一种版本的浏览器中,在保证兼容性的同时,还能最快的得到结果。
最后,总结下,WebAssembly 与 JS 的结合,让 Web 这部车,拥有了超越轿跑的速度。
附:以下是 JS 与 WebAssembly 的性能对比,
相同环境下,分别使用 JS 与 WebAssembly进行斐波那契数列的运算,记录其耗时,当计算量不大时,JS 所用的时间要略少于 WebAssembly,随着计算量的增加,WebAssembly 的优势逐渐增大:
wasm
这是由于,低计算量时,JS与WebAssembly之间的相互调用,需要通过胶水层代码,需要耗费一定的事件,因此无法体现出WebAssembly的优势,但随着计算量的增大,胶水层代码在整个计算过程中所耗时比例逐步降低,此时,WebAssembly的速度优势就体现出来了。
Chrome、Safari 及 Firefox浏览器中,分别使用 JS 与 WebAssembly 复杂数组快排,记录各自的耗时。不同浏览器中,JS 与 WebAssembly 的运行速度相差较大,但在同一浏览器中 WebAssembly 的计算性能都要优于 JS:
wasm
这也体现出了目前三大浏览器对WebAssembly的支持情况。
参考资料: