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 wat
    
  • js中使用

    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.1146484375
    
    pie showData
       title 求和(0-100000)10次均值
       "Rust": 0.0597900390625
       "AssemblyScript": 0.2194580078125
       "java": 0.2195556640625
       "js": 1.3872802734375
    
    pie 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.1047119140625
    
    pie 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,编译简单,但是编码复杂,上手难度较高。

← 返回帖子 更多前端内容 →