POST · 前端 · 文章
wasm 学习
2026/11/07 · by ZHANG XING YUN · 阅读 2 次 · 3311 字 · 预计阅读 9 分钟
一、wasm概述
(一)概述
WASM是一种虚拟指令集体系架构,设计用于将C、C++、Rust等低级语言编译成可在现代网络浏览器中运行的二进制格式。它的主要目标是提高网页应用的性能,使这些应用能够接近或达到原生应用的运行速度,wasm是2015年由Mozilla提出的一个概念,目的是作为一种在Web上运行高性能代码的新格式,2017年各大浏览器厂商开始陆续支持该功能,2019年w3c发布了WebAssembly Core 1.0规范。
(二)优缺点
1、优点
- 高效执行:Wasm的代码更接近机器码,因此执行速度通常比解释型的JavaScript快很多。它利用了现代CPU的特性,如SIMD(单指令多数据)和多线程,以提高计算密集型任务的性能。
- 紧凑的格式:Wasm采用二进制格式,这使得它比文本格式的JavaScript更小,加载更快,占用的网络带宽更少。
- 跨平台性:Wasm被设计为平台无关,这意味着它可以在任何支持它的环境中运行,无论是在浏览器还是在服务器端(如Node.js),甚至在物联网设备上。
- 安全性:Wasm运行在一个沙盒环境中,限制了对宿主系统的直接访问,这增加了安全性并减少了潜在的攻击面。
- 可移植性:Wasm可以由多种高级语言编译生成,如C/C++、Rust、Go等,这使得开发者可以使用他们熟悉的语言来编写高性能的代码。
2、缺点
- 有限的语言特性:Wasm本身不支持一些高级语言特性,如动态类型、面向对象编程、函数重载等,这可能限制了代码的表达力和灵活性。
- 学习成本:虽然Wasm可以由多种语言编译,但理解其内部工作原理和调试Wasm模块可能需要一些学习和适应。
- 生态系统成熟度:尽管Wasm的生态系统正在快速发展,但在某些方面可能不如JavaScript成熟,比如可用的库和框架较少。
- 兼容性和部署:虽然所有主流浏览器都支持Wasm,但在一些老旧或非主流的浏览器中可能不支持,这可能影响到Web应用的广泛部署。
- 调试困难:由于Wasm的二进制性质,直接调试Wasm代码可能比调试JavaScript代码更加困难,尽管工具和调试器正在不断改进这一情况。
(三)使用场景
WASM支持多种应用场景,包括但不限于在线游戏、视频编解码、3D模型渲染、客户端计算机视觉等复杂计算场景。此外,它还支持将现有的桌面应用移植到Web平台,提高用户体验。WASM的实现包括编译过程,即将源代码编译成WASM二进制文件,以及在浏览器中的加载和执行。这个过程可能涉及使用特定的工具链,如Emscripten*(一个工具链,作用是通过LLVM来编译生成asm.js、WebAssembly字节码,目的是让你能够在网页中接近最快的速度运行C和C++,并且不需要任何插件。)*,以及使用JavaScript API与WASM模块进行交互。下面罗列了部分语言转化为wasm的工具,工具很多,github项目awesome-wasm-langs 上罗列个各个语言开发wasm工具,比较详细,可以参考。
二、基本使用
以下使用rust、java、python、assemblyScript演示如何进行编译wasm,从各个语言的目录结构,编译工具到js中使用做了简单的介绍。
需要注意 usize isize 这类根据系统架构决定值大小的类型,wasm默认架构就是32位,哪怕你是64位系统也不行,编译出来也是2^{32} - 1(4,294,967,295)的大小,超出范围,就会发生溢出问题,可以明确类型为i64、 i128、u64、u128,编译之后对应js的类型为bigint。
(一)Rust使用
安装rust下载链接 vscode编辑器下载rust插件
创建rust项目
- rust - src - lib.rs - Cargo.toml安装编译工具
rustup target add wasm32-unknown-unknown cargo add wasm-bindgen // 添加包到项目 cargo install wasm-pack // 安装程序在lib.rs下编写rust代码
use wasm_bindgen::prelude::*; /** * Summation * 求和 */ #[wasm_bindgen] pub fn summation(a: i64, b: i64) -> i64 { let mut sum: i64 = 0; for i in a..b + 1 { sum += i as i64; } sum } /** * 斐波那契数列 * Fibonacci sequence */ #[wasm_bindgen] pub fn fibonacci_optimized(n: i64) -> i64 { // 匹配n的值,对于0和1直接返回它们,因为斐波那契数列的前两个数分别是0和1。 match n { 0 => 0, 1 => 1, // 对于n大于1的情况,使用迭代方法计算第n个斐波那契数。 _ => { // 初始化两个前缀数a和b,初始值分别为0和1。 let mut a = 0; let mut b = 1; // 从2开始迭代到n,计算并更新a和b的值。 for _ in 2..=n { // 计算下一个斐波那契数。 let next = a + b; // 更新a和b的值,将b的值赋给a,将next的值赋给b。 a = b; b = next; } // 返回第n个斐波那契数。 b } } } /** * 使用埃拉托斯特尼筛法找出小于等于上限的所有素数 * Returns all primes less than or equal to the given limit. */ #[wasm_bindgen] pub fn sieve_of_eratosthenes(limit: usize) -> Vec<usize> { // 初始化一个布尔数组,标记每个索引是否为素数。默认假设所有数都是素数。 let mut is_prime = vec![true; limit + 1]; // 0和1不是素数,因此手动标记为false。 is_prime[0] = false; is_prime[1] = false; // 从2开始遍历到sqrt(limit),因为大于sqrt(limit)的数的倍数必然已经被处理过。 for number in 2..=((limit as f64).sqrt() as usize) { // 如果当前数是素数,则将其倍数标记为非素数。 if is_prime[number] { let mut multiple = number * number; // 从当前素数的平方开始,依次标记倍数为非素数,直到超过limit。 while multiple <= limit { is_prime[multiple] = false; multiple += number; } } } // 筛选完成后,过滤出所有标记为素数的索引,并收集为最终的结果。 is_prime .into_iter() .enumerate() .filter_map(|(number, prime)| if prime { Some(number) } else { None }) .collect() }打包生成wasm
wasm-pack build --target web // 打包 在 项目根目录下生成pkg文件夹 包含wasm js ts json 文件使用 直接引入生成的js文件即可
import __wbg_init, {summation} from './pkg/wasm_example.js' (async () => { await __wbg_init(); // 初始化 安装 wasm summation(0, 1000) 使用 })js中使用
import __wbg_init, { summation } from '../rust/rust_wasm.js' (async () => { await __wbg_init() // 函数内部加载wasm 完成后可调用 console.log(summation(0, 10000)); })()
(二)Java使用(Bytecoder)
创建java项目 java-wasm
- java-wasm - src - WasmUse.java - WasmUse.class -java-wasm.imi创建java文件,书写代码
// WasmUse.java import de.mirkosertic.bytecoder.api.Export; import java.util.ArrayList; import java.util.Collections; public class WasmUse { public static void main(String[] args) { } /** * 求和 */ @Export("summation") public static long summation(int a, int b) { long sum = 0; for (int i = a; i <= b; i++) { sum += i; } return sum; } /** * 斐波那契数列 Fibonacci sequence */ @Export("fibonacciOptimized") public static long fibonacciOptimized(int n) { if (n <= 1) { return n; } long a = 0; long b = 1; for (int i = 2; i <= n; i++) { long temp = a + b; a = b; b = temp; } return b; } /** * 使用埃拉托斯特尼筛法找出小于等于上限的所有素数 * Returns all primes less than or equal to the given limit. */ @Export("sieveOfEratosthenes") public static String sieveOfEratosthenes(int limit) { ArrayList<Boolean> isPrime = new ArrayList<Boolean>(Collections.nCopies(limit + 1, true)); isPrime.set(0, false); isPrime.set(1, false); for (int p = 2; p * p <= limit; p++) { if (isPrime.get(p)) { for (int multiple = p * p; multiple <= limit; multiple += p) { isPrime.set(multiple, false); } } } ArrayList<Integer> primes = new ArrayList<Integer>(); for (int i = 2; i <= limit; i++) { if (isPrime.get(i)) { primes.add(i); } } return primes.toString(); } } // pom.xml 增加依赖项 <dependencies> <dependency> <groupId>de.mirkosertic.bytecoder</groupId> <artifactId>bytecoder.api</artifactId> <version>2024-05-10</version> </dependency> </dependencies>编译java代码为class文件
// 编译 javac WasmUse.java // 编译后 WasmUse.class下载bytecoder-cli-xxxx-xx-xx-executable.jar
https://central.sonatype.com/artifact/de.mirkosertic.bytecoder/bytecoder-cli/versions // 上面去找jar包 或者使用 maven 也可以使用bytecoder-cli-xxxx-xx-xx-executable.jar编译为js or wasm
java -jar bytecoder-cli-2024-05-10-executable.jar compile wasm -classpath=. -mainclass=WasmUse -builddirectory=./java // 需要事先创建指定输出的文件夹生成bytecoderwasmclasses.wasm bytecoderruntime.js bytecoderwasmclasses.wat 文件
js中使用
// 生成的bytecoderruntime.js 增加export 导出 export const bytecoder = { ... }import { bytecoder } from '../java/bytecoderruntime.js' bytecoder.instantiate('../java/bytecoderwasmclasses.wasm').then(() => { // 调用bytecoder封装的方法,安装wasm const { summation } = bytecoder.instance.exports // 安装完成会将wasm方法注册到bytecoder.instance.exports上 console.log(summation(null, 0, 100)); // 调用 第一个参数unuser 编译器编译出来就是这样 默认传null })
(三)Python使用(编译不了🙂[心累])
- 累累累
(四)AssemblyScript使用官网
初始化项目
npm init // 初始化npm npm install --save-dev assemblyscript // 安装 assemblyscript 编译器 npx asinit . // 脚手架创建新项目生成项目目录结构
- assembly // 编写 wasm 代码文件夹 - index.ts - tsconfig.json - build // 打包之后的wasm - .gitignore - tests // 测试代码 - index.js - asconfig.json // assemblyscript 配置相关 - index.html编写代码 类似 ts
// The entry file of your WebAssembly module. /** * 求和 */ export function summation(a: usize, b: usize): usize { let sum: usize = 0; for (let i = a; i <= b; i++) { sum += i; } return sum; } /** * 斐波那契数列 * Fibonacci sequence */ export function fibonacci_optimized(n: usize): usize { if (n <= 1) { return n; } let a: usize = 0; let b: usize = 1; for (let i: usize = 2; i <= n; i++) { let temp: usize = a + b; a = b; b = temp; } return b; } /** * 使用埃拉托斯特尼筛法找出小于等于上限的所有素数 * Returns all primes less than or equal to the given limit. */ export function sieveOfEratosthenes(limit: usize): Array<usize> { const isPrime: Array<boolean> = new Array<boolean>(limit + 1).fill(true); isPrime[0] = false; isPrime[1] = false; for (let p: usize = 2; p * p <= limit; p++) { if (isPrime[p]) { for (let multiple = p * p; multiple <= limit; multiple += p) { isPrime[multiple] = false; } } } const primes = []; for (let i: usize = 2; i <= limit; i++) { if (isPrime[i]) { primes.push(i); } } return primes; }编译打包
npm run asbuild // 生成对应的 wasm 到 build 文件夹下 js ts wasm watjs中使用
import { summation } from '../ascript/release.js' console.log(summation(0, 100)); // 导入时已经注册完成了,可以直接使用=
(五)wat文件编译
wat文件是编写wasm使用的语言,类似汇编,可以使用wat2wasm编译
// simple.wat
(module // 模块
(func $i (import "imports" "imported_func") (param i32)) // 导入一个函数
(func (export "exported_func") // 导出一个函数
i32.const 42 // 函数体 指令组成
call $i
)
)
编译生成wasm文件:
npm inatall wat2wasm -g // 安装wat2wasm
wat2wasm simple.wat -o simple.wasm // 编译输出
js安装使用
const importObject = {
imports: { imported_func: (arg) => console.log(arg) }, // simple.wat文件导入的模块需要定义
};
fetch("simple.wasm")
.then((res) => res.arrayBuffer())
.then((bytes) => WebAssembly.instantiate(bytes, importObject)) // 安装第一个参数 buffer类型 第二个参数安装时导入的模块
.then((results) => {
results.instance.exports.exported_func(); // 返回的结果调用导出的方法
});
三、性能对比
针对以上几种语言分别采用类似的方法各书写了三个函数,分别是求和、计算斐波那契数列、使用埃拉托斯特尼筛法找出小于等于上限的所有素数,这三个函数都是利用循环,没有在语言层面做过多优化,分别计算改函数耗时来做性能对比。
求和
pie showData title 求和(0-100)10次均值 "AssemblyScript": 0.029638671875 "java": 0.0457275390625 "Rust": 0.0860107421875 "js": 0.1146484375pie showData title 求和(0-100000)10次均值 "Rust": 0.0597900390625 "AssemblyScript": 0.2194580078125 "java": 0.2195556640625 "js": 1.3872802734375pie showData title 求和(0-100000000)10次均值 "Rust": 0.049853515625 "js": 101.0948486328125 "java": 169.2158935546875 "AssemblyScript": 170.6884765625
根据上面三个图表可以看出,在数据量不大的情况下,四种语言耗时最长的是js,最短的是AssemblyScript,相差1毫秒左右,在计算量逐渐增大的情况下,rust耗时最短,java和AssemblyScript差不多,浮动也差不多,rust比较稳定。
斐波那契数列
pie showData title 斐波那契数列(10)10次均值 "AssemblyScript": 0.0260009765625 "java": 0.0385986328125 "Rust": 0.0660888671875 "js": 0.1047119140625pie showData title 斐波那契数列(80)10次均值 "AssemblyScript": 0.0279541015625 "java": 0.0368896484375 "Rust": 0.0787841796875 "js": 0.1114501953125斐波那契数列计算耗时都在1毫秒以内,相差不是很大,可以看到,耗时不严重的任务AssemblyScript和java是有优势的。
质数
pie showData title 质数(100)10次均值 "AssemblyScript": 0.2164794921875 "js": 0.297705078125 "Rust": 0.3380859375 "java": 0.417041015625
pie showData
title 质数(100000)10次均值
"Rust": 1.3346435546875
"js": 6.5036865234375
"AssemblyScript": 6.5539794921875
"java": 46.239697265625
在质数这个测试中java明显拉跨,原因在于java返回的值在java层面做了toString处理,不然js层面输出不了。因为java和js数据类型需要转换,在编译wasm时对java数据格式用js做的映射,没有找到ArrayList转js Array的方法,这块java可以忽略。
可以看到rust是最稳定的,没有太大的浮动,甚至在求和计算时数据越大,rust耗时越少。AssemblyScript和java在耗时上相差不大,计算量不大的情况下,首选还是js,耗时没有多大差别,何必耗时耗力去整wasm呢。在计算复杂耗时严重的情况下,可以考虑使用AssemblyScript,基于ts较为简单,编译的结果也有ts声明。使用java的话编译比较复杂,编写也比较耗时,编译后不支持ts,不推荐。最推荐的是rust,编译简单,但是编码复杂,上手难度较高。