让我来看看wasm你小子到底是个啥

1、背景

1.1 WASM到底是什么?

WASM全称WebAssembly,它不是一种编程语言,而是一种中间代码或是我们常说的字节码(IR),是一些高级语言的编译目标。关于字节码相关知识可以查看我另一篇文章,V8引擎 - JS执行原理。因此它其实是JS的一项补充,而不是替代。

WebAssembly 是一种运行在现代网络浏览器中的新型代码,并且提供新的性能特性和效果。它设计的目的不是为了手写代码而是为诸如 C、C++ 和 Rust 等低级源语言提供一个高效的编译目标。(摘录自MDN)

1.2 我们为什么要用WASM?

WASM设计的目标是在 web 浏览器中运行应用程序的性能接近与在原生环境中运行应用程序的性能。结合上面的文章我们知道,在V8引擎中执行JS时,JS代码经过了解释器和编译器两个步骤,无论是在解释成字节码,还是把热代码编译成机器码,这个过程都是在运行时执行的,会带来一定的性能成本。而把一些代码提前编译成了Wasm,浏览器(主流浏览器基本已经支持)只要拥有WASM的虚拟机,就能直接执行对应的字节码,节省了解释/编译步骤。

当然,Wasm的好处不仅仅有这个,下面是 Wasm的一些主要特性:

  1. 安全:WASM 提供了沙盒环境,用于在 web 页面中执行代码,以保护系统免受恶意软件的攻击。
  2. 小巧且快速:WebAssembly 的二进制格式小巧,载入快速,解码效率高,可以有效舒缓网络瓶颈、加快下载速度。
  3. 高效:WASM 能够利用现代硬件的功能,在浏览器环境中实现接近原生代码的执行效率。
  4. 语言支持:WASM 不仅仅是 JavaScript 的替代品,它也为 C、C++、Rust 等语言提供了一种在 Web 平台上运行的方式。
  5. 可移植:WASM 是开发跨平台应用的理想选择,因为它被设计为在所有主要的浏览器和操作系统上运行。

其实上面的大部分特性都与虚拟机有关,当然wasm最大的兼容性问题也在浏览器是否支持这个虚拟机上,幸运的是大部分主流厂商都已经支持了。

1.3 那么我们要怎么使用呢?

目前,Wasm主要支持C/C++和Rust,我们如果需要编写Wasm模块的话,就需要学习这几门语言。当然,这也给我们一个快速复用现有轮子的机会,我们可以把一些成熟开源的C/C++和Rust模块编译成为Wasm直接使用。当然在实际的使用场景,我们大多数时间还是需要自己编写,下面我们就来一步一步的实现一个wasm的demo。

2、实践出真知

2.1 准备工作

C/C++在大学时期由于有一定的学习,因此这次由于想尝试一下新的语言,所以本次demo将会以Rust作为开发语言。其实C/C++和Rust分别都有自己编译Wasm的工具,分别是Emscripten和wasm-pack。大家可以选择喜欢的语言进行开发,编译完成后,使用的模块的方法基本是一致的。

1、环境安装

(1)Rust环境安装

命令行输入:

javascript 复制代码
curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh

过程中会让你选择安装模式,选择第一项默认安装即可。

最终当终端中输出 "Rust is installed now. Great!" 即安装成功,也可以通过输入如下命令来验证是否已经安装成功:

javascript 复制代码
rustc -V
cargo -V

如果执行有问题,就看下终端里面是否有PATH环境变量相关提示,如果有的话则执行提示的命令:

javascript 复制代码
source $HOME/.cargo/env
  • rustc:rustc是Rust 的标准编译器命令,它用于将你写的 Rust 代码编译成可执行文件。但实际的工作中我们用的比较多的是cargo命令。
  • cargo:cargo是rust的包管理工具,相当于我们js中的npm。

(2)wasm-pack安装

wasm-pack是我们后续编译wasm模块的工具,直接在项目目录下执行以下命令安装:

javascript 复制代码
cargo install wasm-pack

(3)常规环境配置

由于需要构建,这里我直接使用vue-cli来初始化工程,减少后续配置量,大家可以按需配置:

js 复制代码
环境信息
node: "14.18.1"
npm: "6.14.15"
json 复制代码
"@babel/core": "^7.12.16",
"@babel/eslint-parser": "^7.12.16",
"@vue/cli": "^5.0.8",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"eslint": "^7.32.0",
"eslint-plugin-vue": "^8.0.3",
"vue-template-compiler": "^2.6.14"

2、目录初始化

接下来我们把需要的目录进行初始化。

(1)wasm模块目录初始化

在你的工程根目录下执行以下命令,此时会在根目录新建一个wasm-lib的文件夹,用于存放wasm模块相关文件。

powershell 复制代码
cargo new wasm-lib

对应的目录结构如下:

打开./wasm-lib/src/main.rs可以发现,cargo已经帮我们创建好一个实例代码新建完成:

3、编译执行项目

在wasm-lib目录下直接执行以下命令:

powershell 复制代码
cargo run

可以看到此时我们的wasm-lib中新增了一个target目录(编译后的文件存放在这里),并且在终端中成功打印了Hello,world。至此我们的准备工作基本完成,接下来我们就可以开始做我们自己的业务修改了。

2.2 一个工具函数实践

接下来我们就可以直接在刚才的 main.rs 文件中直接实现工具函数,给接下来的业务js调用。

1、实现工具函数

这里我们先实现一个简单的把数组内所有项加起来的函数:

目录:wasm-rust/wasm-lib/src/main.rs

rust 复制代码
// 导入 wasm-bindgen crate,它是在 Rust 和 JavaScript 之间做桥接的工具
use wasm_bindgen::prelude::*;
// 导入 JsCast trait,它提供了将 JavaScript 值和 Rust 值相互转换的方法
use wasm_bindgen::JsCast;
// 导入 JavaScript 的数组类型
use js_sys::Array;
// 导入 JavaScript 的数值类型
use js_sys::Number;

// 使用 wasm_bindgen 注解来让这个函数在 JavaScript 中可以调用
#[wasm_bindgen]
// 定义一个名为 sum 的公开函数,接收一个 JavaScript 的数组作为参数,返回一个 f64 类型的值
pub fn sum(arr: Array) -> f64 {
    // 初始化求和结果的变量为 0.0
    let mut sum = 0.0;

    // 遍历输入数组中的每个元素
    for i in 0..arr.length() {
        // 尝试获取数组的第 i 个元素,并尝试将它从 JsValue 转换为 Number 类型
        // 如果转换成功,.ok() 方法会返回 Some(Number)
        // 如果转换失败(例如该元素不是一个数值),.ok() 方法会返回 None
        if let Some(val) = arr.get(i).dyn_into::<Number>().ok() {
            // 如果成功获取并转换元素,就把它加到总和上
            sum += val.value_of();
        } 
        // 如果无法获取或转换元素(例如该元素不是一个数值),可以给个else处理这种情况
    }
    // 返回求和后的结果
    sum
}

由于上面我们引用了一些rust模块进行处理,因此我们需要修改一下Cargo.toml

目录:wasm-rust/wasm-lib/Cargo.toml

rust 复制代码
[package]
name = "wasm-lib"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
name = "main"
path = "src/main.rs"
crate-type = ["cdylib", "rlib"]

[dependencies]
wasm-bindgen = "0.2"
js-sys = "0.3"

2、编译

为了后续编译方便,我们在工程根目录的package.json的scripts中加入以下指令:

javascript 复制代码
"build:wasm": "cd wasm-lib && wasm-pack build --target web --out-dir js-pkg"

在根目录下执行以上命令,就会开始进行编译,编译成功后可以在wasm-rust/wasm-lib/js-pkg找到对应的产物。

可以看到,js-pkg里面一共有四个产品:分别以wasm,wasm.d.ts,ts,js结尾。他们分别的作用是:

  • wasm文件:这是由 Rust 编译器生成的 WebAssembly 二进制文件。它包含你的 Rust 代码编译后的 WebAssembly 二进制指令。这就是浏览器或其他 WebAssembly 运行时实际加载并执行的文件。
  • .wasm.d.ts: 这是一个 TypeScript 定义文件,它定义了 wasm 文件中暴露出的所有函数和类型的 TypeScript 类型。这允许 TypeScript 开发者在编译时间检查代码是否正确地使用了 wasm 文件。
  • .ts: 这是一个 TypeScript 源代码文件,它包含用于与 wasm 文件交互的 TypeScript 代码。这个文件中的代码负责在 JavaScript 运行时加载和初始化 wasm 文件,然后将 wasm 中的函数和类型绑定到 JavaScript 对象上,让 JavaScript 代码可以调用。
  • .js: 这是一个 JavaScript 文件,它是 .ts 文件的编译结果。这就是浏览器或其他 JavaScript 运行时实际加载和执行的文件。

在浏览器运行时,先是加载js文件,js文件会请求.wasm文件,然后在webassembly的虚拟中加载执行。两个ts文件主要是帮助开发者进行开发和定位问题的。

3、在js中引入并使用

在业务代码中直接把对应的main.js入口文件import进行使用即可。

这里要注意几个点:

  1. 编译好的wasm是一个异步模块,并且一定会抛出一个init的promise。
  2. 在使用wasm模块时,需要导入init,并且等执行完后,再来使用导出的函数。
    1. 这里可以使用init().then()或者await来处理。
  3. 除了使用相对路径引入之外,由于这个编译好的wasm模块是独立,你也可以使用npm包的方式install进来,然后使用。
javascript 复制代码
<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png">
    <HelloWorld msg="Welcome to Your Vue.js App"/>
  </div>
</template>

<script>
import HelloWorld from './components/HelloWorld.vue'
import init, { sum } from './../wasm-lib/js-pkg/main.js'

export default {
  name: 'App',
  components: {
    HelloWorld
  },
  methods: {
    async jsInit () {
      // 编译好的wasm是一个异步模块,并且抛出的是一个promise,我们可以init().then()或await使用。
      await init()
      let js_sum = sum([1,2,3,4,6])
      console.log(js_sum)
    }
  },
  mounted: function() {
    console.log('start')
    this.jsInit()
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

执行后控制台成功输出对应的结果:

4、性能测试

完成了上面的内容,一个简单的demo已经完成了,接下来我们就可以测试wasm一直引以为傲的性能优势了。

(1)wasm性能怎么更差了?

我们把相同的算法,用js实现一次,并且在执行对应的算法前后打桩,计算执行时间:

javascript 复制代码
<script>
import HelloWorld from './components/HelloWorld.vue'
import init, { sum } from './../wasm-lib/js-pkg/main.js'

let arr = Array.from({length: 30000}, () => Math.floor(Math.random() * 100) + 1);
let js_sum_cal = function() {
    let sum = 0;

    for (let i = 0; i < arr.length; i++) {
        let val = arr[i];
        sum += val;
    }
    return sum;
}

export default {
  name: 'App',
  components: {
    HelloWorld
  },
  methods: {
    jsInit () {
      let js_start = performance.now()
      let js_sum = js_sum_cal(arr)
      let js_end = performance.now()
      console.log(`js结果:${js_sum}`)
      console.log(`js耗时:${js_end - js_start}`)
    },
    async wasmInit () {
      // 编译好的wasm是一个异步模块,并且抛出的是一个promise,我们可以init().then()或await使用。
      await init()
      let wasm_start = performance.now()
      let wasm_sum = sum(arr)
      let wasm_end = performance.now()
      console.log(`wasm结果:${wasm_sum}`)
      console.log(`wasm耗时:${wasm_end - wasm_start}`)
    }
  },
  mounted: function() {
    console.log('start')
    this.jsInit()
    this.wasmInit()
  }
}
</script>

我们使用上面的代码执行10次得到的平均值,我们发现wasm在数组长度为30000时,大概会比JS计算速度慢10倍,并且随着量级变大这个差距会逐步变大。

以下为其中一次的结果。

(2)到底问题出在哪?

what!??说好的wasm性能拉满呢?搞半天白忙活了?

经过一番gpt,搜索引擎苦战后,我找到的理论依据:

WebAssembly (Wasm) 的主要优势在于它的计算性能,特别是对于像图形渲染、物理模拟、数据处理和编解码这样的高计算任务,这也是为什么它被用于游戏、音视频编辑器、图像识别等高效能应用。

然而,对于更简单的任务,如数组求和,WebAssembly 通常与 JavaScript 的执行速度相近,有时甚至可能比 JavaScript 慢。这是由多个因素引起的:

  1. 转换开销:由于 WebAssembly 是一种更接近于机器代码的语言,与 JavaScript 不同,JavaScript 和 WebAssembly 之间的交互涉及到了类型转换和复制数据。任何需要架起在两者之间的通信(比如传递参数,得到结果等)都需要一些开销。

  2. JavaScript 引擎优化:现代浏览器的 JavaScript 引擎,如 Chrome 的 V8 或 Firefox 的 SpiderMonkey,经过多年的优化,已变得非常快。对于一些简单的代码,特别是像数组求和这样的代码,JavaScript 引擎可以进行高度优化,甚至可能使它比 WebAssembly 小巧的表示和线性内存模型更快。

    而我们设计的场景正好就命中了情况一,我们通过通信把一个很大的数组传入了wasm模块,并且经过wasm模块的转换,这个转换的时间远远大于算法执行的时间,所以会出现上面的性能问题。

3、总结一下

这篇文章我们学习了解了一下wasm的一些基础特点以及应该如何使用。并且结合具体的业务工程,实践从0到1编写,编译,引用的rust编译好的wasm模块。最后我们对wasm的性能问题进行了一定的分析。希望这篇文章能帮助到大家更好的了解到wasm模块的使用的。

参考链接

相关推荐
J不A秃V头A36 分钟前
Vue3:编写一个插件(进阶)
前端·vue.js
司篂篂1 小时前
axios二次封装
前端·javascript·vue.js
姚*鸿的博客1 小时前
pinia在vue3中的使用
前端·javascript·vue.js
宇文仲竹2 小时前
edge 插件 iframe 读取
前端·edge
Kika写代码2 小时前
【基于轻量型架构的WEB开发】【章节作业】
前端·oracle·架构
天下无贼!3 小时前
2024年最新版Vue3学习笔记
前端·vue.js·笔记·学习·vue
Jiaberrr3 小时前
JS实现树形结构数据中特定节点及其子节点显示属性设置的技巧(可用于树形节点过滤筛选)
前端·javascript·tree·树形·过滤筛选
赵啸林3 小时前
npm发布插件超级简单版
前端·npm·node.js
罔闻_spider4 小时前
爬虫----webpack
前端·爬虫·webpack
吱吱鼠叔4 小时前
MATLAB数据文件读写:1.格式化读写文件
前端·数据库·matlab