webassembly:小伙子,你渴望力量吗?

7,111 阅读9分钟

本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

hello,小伙伴们好呀,最近偷偷学习了一下webassbmbly,感觉这个东西还不错,有很大的发展前景,所以出了这期简单的教程~

WebAssembly是什么

WebAssembly(wasm),是一种新的编码方式,可以在现代的网络浏览器中运行。它是一种低级的类汇编语言,具有紧凑的二进制格式,可以接近原生的性能运行。并为诸如 C / C ++/rust等语言提供一个编译目标,以便它们可以在 Web 上运行。它也被设计为可以与 JavaScript 共存,允许两者一起工作。简单来说,就是咱们可以在浏览器环境中运行由c/c++/rust等语言编译后wasm文件

结论: wasm 是一个可移植、体积小、加载快并且兼容 Web 的全新格式。

emmm,那这个webassembly或者说wasm又有什么用呢?

img

嘿嘿,这用途可大着嘞。

比如咱们可以运行一些原来在javascript中没有的库,比如ffmpegopencv等库咱们可以将其编译成wasm文件,然后运行在浏览器环境。

再者,人们常说21世纪会是AI的世纪,但浏览器的性能太垃,无法做一些AI识别相关的东西。那现在有一个可以提升在浏览器中的运算性能的方案,又可以使用c++相关的一些深度学习库,那是不是,嘿嘿嘿。没错,目前国内外的一些深度学习框架都在这方面进行了一些探索,比如tensorflow.jspaddle.js等。

那WebAssembly这么厉害,它可不可以取代javascript呀?

答案是否定的,咱们希望WebAssembly和JavaScript齐头并进,WebAssembly作为前端的一个扩展的方向即可。

ok,看到这里,那咱们基本上可以得出这么一个结论:webAssembly主要是作为前端的扩展,在性能优化复用后端的一些库上都是比较出色的。

那么对于WebAssembly的兼容性如何呢?

在can i use 中咱们可以看到基本上pc端的浏览器的都兼容了(除了微软都已经放弃的ie),但是移动的兼容性还是比较欠缺的。因此在pc端开发者可以放心食用,移动端的web开发者建议再观望一下~

image-20220727232643325

wasm的前世今生

在业务需求越来越复杂的现在,前端的开发逻辑越来越复杂,相应的代码量随之变的越来越多,随之而来的也就前端性能的瓶颈。在项目的运行过程中,引擎会对执行次数较多的function记性优化,引擎将其代码编译Machine Code后打包送到顶部的Just-In-Time(JIT) Compiler,下次再执行这个function,就会直接执行编译好的Machine Code。但是由于JavaScript的动态变量,上一秒可能是Array,下一秒就变成了Object。那么上一次引擎所做的优化,就失去了作用,此时又要再一次进行优化。

因此wasm的前生asm.js就诞生啦,asm.js强制静态类型,是一个Javascript的严格子集,合理合法的asm.js代码一定是合理合法的JavaScript代码,但是反之就不成立。但是asm.js还是没能逃过编译成Machine Code这个耗时巨大的步骤。

在2015年,wasm诞生了,它是经过编译器编译之后的代码(因此会比asm.js快),体积小、起步快。在语法上完全脱离JavaScript,同时具有沙盒化的执行环境。

为什么wasm那么快

大概有以下几点原因:

  • 抓取 WebAssembly 比 JavaScript 花费的时间更少,哪怕当它们都被压缩过。
  • 编码 WebAssembly 比解析 JavaScript 所花费的时间更少。
  • WebAssembly 比 JavaScript 更加接近机器码而且在服务端就已经经过了优化,所以它编译和优化需要的时间更少。
  • WebAssembly 不需要重优化,因为它有明确的类型以及内置的额外信息,所以 JS 引擎不需要像优化 JavaScript 那样对它进行推测。
  • 执行阶段花费的时间更少,开发者不必为了写出性能一致性更高的代码而去了解一些编译器的技巧和陷阱。而且 WebAssembly 的一系列的只能对机器来说更加理想。
  • 不需要垃圾回收机制,因为内存都是手动管理的。

聊了这么多wasm的知识,咱对wasm也算是有了一个初步的了解,那么wasm该怎么使用呢?下面跟着小羽的步伐,来完成你的第一个wasm小demo吧~

安装rust

俗话说:磨刀不误砍柴工,读完大学再打工。

啊呸,装完环境再开发。

Emmm,这里为啥是选用rust呢?因为rust对wasm以及现在的前端工程实在是太友好了呀,为啥这么说呢,读者大哥们,请听在下后面给你们慢慢道来~

对于rust的安装在mac/linux上的安装十分简单,一行命令即可

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

然后注入环境变量,接着输入rustc --versioncargo --version,检验是否安装成功

source "$HOME/.cargo/env"

image-20220727234419706

输入echo $HOME/.cargo/,查看cargo的安装路径,然后在在改路径下添加config文件,config文件内容如下:

[source.crates-io]
replace-with = 'tuna'[source.tuna]
registry = "https://mirrors.tuna.tsinghua.edu.cn/git/crates.io-index.git"

ok,咱们安装完了rust,相关的环境,那咱们再安装一下rust下的wasm开发和打包相关的环境吧

# 安装编译打包工具
cargo install wasm-pack
# 安装脚手架工具
cargo install cargo-generate 

到这里咱们终于把所有的准备工作都搞定了,那咱们就先开始搞一个简单的小demo来玩玩吧。

img

编写第一个wasm程序

对于wasm不熟悉的同学怎么办呢?那肯定使用前人使用过的模版/脚手架呀,vue有vue-cli、react有creat-react-app,那咱们wasm也一样可以有的,输入如下的命令,然后提示输入项目的名称,咱们输入myWasm

cargo generate --git https://github.com/rustwasm/wasm-pack-template

image-20220728002556105

修改src下的lib.rs文件,然后构建wasm,wasm-pack build

首次构建是,会安装一些相关的依赖,需要稍微等待一下。

然后就可以在咱们的根目录下看到咱们需要的wasm二进制文件和相应的js胶水代码

mod utils;
​
use wasm_bindgen::prelude::*;
​
// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global
// allocator.
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
​
#[wasm_bindgen]
extern {
    fn alert(s: &str);
}
​
#[wasm_bindgen]
pub fn greet() {
    alert("Hello, xiaoyu-wasm!");
}

image-20220802103348899

image-20220802142046153

接着,咱们,生成调试页面npm init wasm-app www

此时,项目的整体结构如下

image-20220802142432660

进入目录,并安装相关依赖

image-20220802102020044

然后启动项目,npm run start,访问localhost:8081

嗯,弹出了一个alert弹窗,没毛病,搞定

image-20220802142728222

哼哼,小伙子们,是不是发现有什么不对劲的地方?

没错,咱们在rust中是想让他弹出一个alert弹窗,但是里面的内容是Hello, xiaoyu-wasm!,这弹出的是啥玩意啊。

回到www/index.js文件中,咱们就可以发现了端倪

原来这个是模板文件默认引用了一个叫hello-wasm-pack的npm包。注意,这个包时将wasm-pack的默认打包后的目录发布到了npm上。那这说明了什么,咱们完全可以将打包完后的东西发布到npm上,提供给开发者们使用!!!看,这是不是和咱们刚开始说的对前端工程化很友好的呀~

emmm,先将引用换成咱们相关的文件来看看。

修改www/index.js文件,然后刷新页面,发现alert的弹出内容已经修改为了Hello, xiaoyu-wasm!,这不就对了嘛。

// import * as wasm from "hello-wasm-pack";
​
// wasm.greet();
​
import * as myWasm from '../pkg';
myWasm.greet()
​

image-20220802143541830

性能对比

上个小节咱们完成了一个wasm的一个小demo,那wasm在咱们的日常开发中有什么用呢?

看文档上说,wasm会比原生的js拥有更好的性能

ok,那咱们请出咱们性能测试的老朋友——斐波那契数列

修改src/lib.rs文件,然后重新打包wasm-pack build

mod utils;
​
use wasm_bindgen::prelude::*;
​
// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global
// allocator.
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
​
#[wasm_bindgen]
extern {
    fn alert(s: &str);
}
​
#[wasm_bindgen]
pub fn greet() {
    alert("Hello, xiaoyu-wasm!");
}
​
#[wasm_bindgen]
pub fn fb(i: i32) -> i32 {
    return if i <= 2 { 1 } else { fb(i - 1) + fb(i - 2) };
}
​

修改www/index.js,向window中注入jsFbwasmFb方法

// import * as wasm from "hello-wasm-pack";// wasm.greet();import * as myWasm from '../pkg';
// myWasm.greet()function wasmFb(n){
  return myWasm.fb(n)
}
​
function jsFb(n) {
  if (n === 1 || n === 2) {
      return 1;
  }
  return jsFb(n - 1) + jsFb(n - 2)
}
​
window.jsFb = jsFb
window.wasmFb = wasmFb

修改www/index.html,添加一个测试的按钮,循环调用js和wasm方法获取40-45位的斐波那契数列。

ps:由于递归斐波那契数列在数据量小的时候,可能会存在cpu波动的原因,从而导致结果不那么准确,因此从40开始测试。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Hello wasm-pack!</title>
  </head>
  <body>
    <noscript>This page contains webassembly and javascript content, please enable javascript in your browser.</noscript>
    <script src="./bootstrap.js"></script>
    <script>
​
      function handleTest(){
        const testJs = (num)=>{
          console.time(`js 斐波那契数列第${num}位耗时`)
          const res = window.jsFb(num)
          // console.log(`斐波那契数列第${num}位结果:`,res)
          console.timeEnd(`js 斐波那契数列第${num}位耗时`)
        }
        const testWasm = (num)=>{
          console.time(`wasm 斐波那契数列第${num}位耗时`)
          const res = window.wasmFb(num)
          // console.log(`斐波那契数列第${num}位结果:`,res)
          console.timeEnd(`wasm 斐波那契数列第${num}位耗时`)
        }
        const list = [40,41,42,43,44,45]
        for(let item of list){
          testJs(item)
          testWasm(item)
          console.log('%c *************************** 分割线 ***************************', 'color:#fff; background:#00897b ')
        }
      }
    </script>
    <button onclick="handleTest()" >测试</button>
  </body>
</html>

测试结果如下图,可以看出调用wasm递归获取斐波那契数列的耗时基本只需要js的一半时间。

40位的时候可以减少约420ms

41位的时候可以减少约660ms

42位的时候可以减少约1080ms

43位的时候可以减少约1600ms

44位的时候可以减少约2800ms

45位的时候可以减少约4600ms

基本上可以说明wasm在需要cpu运算越长的算法中,优化的效果越明显。

image-20220802152603160

注意事项

老实说,目前90%以上的场景都可以不需要用到wasm,基本上优化算法逻辑就可以解决大部分的问题。在运算时间100ms以下的场景中没有太大的必要性。因此目前wasm通常使用在图片音视频、以及一些原有的非js库场景,或者在某些运算量很大的场景。

小结

本文小羽和小伙伴们学习了wasm相关的一些内容,并且基于rust实现了一个简单的demo。但以目前wasm生态而言,还没有太多的库可以提供直接使用(ffmpeg.js、opencv.js、TensorFlow.js)。但如果是做图片音视频AI相关的同学,可以往这方面进行一些尝试,可以得到不错的性能提升

如果看这篇文章后,感觉有收获的小伙伴们可以点赞+收藏哦~

img

如果想和小羽交流技术可以加下wx,也欢迎小伙伴们来和小羽唠唠嗑,嘿嘿~

最后透露一下,目前小羽已经联系TF男孩。如果不出意外的话,咱们会合作输出一期AI+wasm实战的文章,小伙伴敬请期待哦~