Wasm 真的比 Js 快吗?

网上各种介绍wasm的文章都在说wasm的性能是js的数倍之多,实际情况真的是这样吗?

本文用rust编译到wasmjs进行性能对比,尝试解答这个问题

普通计算

直接上代码,如果是计算斐波那契数列

rust 代码如下

rust 复制代码
#[wasm_bindgen]
pub fn fib_wasm(value: u32) -> u32 {
    if value <= 1 {
        return value;
    }

    fib_wasm(value - 1) + fib_wasm(value - 2)
}

运行 wasm-pack build --verbose --release --target web 编译成 js

相同代码用 js 编写,代码如下所示

typescript 复制代码
function fibJs(value: number): number {
  if (value <= 1) {
    return value
  }
  return fibJs(value - 1) + fibJs(value - 2)
}

测试代码如下

javascript 复制代码
const maxIterTime = 10

const testFib = (fib: (value: number) => number) => {
  const result = []
  for (let i = 10; i < 10 + maxIterTime; i++) {
    result.push(fib(i))
  }
}

console.time("js")
testFib(fibJs)
console.timeEnd("js")

console.time("wasm")
testFib(fibWasm)
console.timeEnd("wasm")

对比结果如下

可以看到js的耗时确实是wasm的数倍之多, 但是由此就能得到结论wasmjs快吗?

现实场景

考虑一个真实的场景,计算凸包, 因为笔者工作原因,最近有用到该算法,就拿该算法来对比

本文不讨论该算法的细节,后面会出一篇文章专门来介绍该算法的实现

编写一个 graham_scan 算法, 核心的rust 代码如下

rust 复制代码
#[wasm_bindgen]
#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
pub struct Vector {
    x: f64,
    y: f64,
}

impl Sub<&Vector> for &Vector {
    type Output = Vector;

    fn sub(self, rhs: &Vector) -> Self::Output {
        Vector {
            x: self.x - rhs.x,
            y: self.y - rhs.y,
        }
    }
}

fn cross_product(a: &Vector, b: &Vector) -> f64 {
    a.x * b.y - b.x * a.y
}

pub fn graham_scan(mut array: Vec<Vector>) -> Vec<Vector> {
    if array.len() <= 3 {
        return array;
    }

    let mut p0 = &array[0];

    // find min y point assign p0
    for p in array.iter().skip(1) {
        if p.y < p0.y {
            p0 = p;
        } else if p.y == p0.y && p.x < p0.x {
            p0 = p
        }
    }

    let p0 = p0.clone();

    fn get_polar_angle(v: &Vector) -> f64 {
        (v.y).atan2(v.x)
    }

    array.sort_by(|a, b| {
        let a_p0 = a - &p0;
        let b_p0 = b - &p0;
        let polar_angle_a = get_polar_angle(&a_p0);
        let polar_angle_b = get_polar_angle(&b_p0);
        match polar_angle_a.partial_cmp(&polar_angle_b).unwrap() {
            Ordering::Equal => {
                let dist_a = a_p0.x.powf(2.) + a_p0.y.powf(2.);
                let dist_b = b_p0.x.powf(2.) + b_p0.y.powf(2.);
                (dist_a).partial_cmp(&dist_b).unwrap()
            }
            v => v,
        }
    });

    let mut result: Vec<Vector> = vec![p0];

    for p in array.iter().skip(1) {
        loop {
            let len = result.len();
            if len >= 2 {
                let p1 = &result[len - 1];
                let p2 = &result[len - 2];
                if cross_product(&(p1 - p2), &(p - p1)).is_sign_negative() {
                    result.pop();
                    continue;
                }
            }
            break;
        }
        result.push(p.clone());
    }

    result
}

相同的js代码如下

typescript 复制代码
type Vector = {
  x: number
  y: number
}

function grahamScan(array: { x: number; y: number }[]) {
  if (array.length <= 3) {
    return array
  }

  let p0 = array[0]

  for (let i = 1; i < array.length; i++) {
    const p = array[i]
    if (p.y < p0.y) {
      p0 = p
    } else if (p.y == p0.y && p.x < p0.x) {
      p0 = p
    }
  }

  function get_polar_angle(v: { x: number; y: number }) {
    return Math.atan2(v.y, v.x)
  }

  array.sort((a, b) => {
    const a_p0 = createVector(a, p0)
    const b_p0 = createVector(b, p0)
    const polar_angle_a = get_polar_angle(a_p0)
    const polar_angle_b = get_polar_angle(b_p0)
    if (polar_angle_a == polar_angle_b) {
      const dist_a = a_p0.x ** 2 + a_p0.y ** 2
      const dist_b = b_p0.x ** 2 + b_p0.y ** 2
      return dist_a - dist_b
    }
    return polar_angle_a - polar_angle_b
  })

  const result = [p0]

  for (let i = 1; i < array.length; i++) {
    const p = array[i]
    while (result.length >= 2) {
      const length = result.length
      const p1 = result[length - 1]
      const p2 = result[length - 2]
      if (crossProduct(createVector(p1, p2), createVector(p, p1)) < 0) {
        result.pop()
        continue
      }
      break
    }
    result.push(p)
  }
  return result
}

function createVector(a: Vector, b: Vector) {
  return {
    x: a.x - b.x,
    y: a.y - b.y,
  }
}

function crossProduct(a: Vector, b: Vector) {
  return a.x * b.y - a.y * b.x
}

测试用的样本文件, 该样本是从leetcode上拷贝下来的

javascript 复制代码
const data = [
  [0, 2],[0, 4],[0, 5],[0, 9],[2, 1],[2, 2],[2, 3],[2, 5],[3, 1],[3, 2],[3, 6],[3, 9],[4, 2],[4, 5],[5, 8],[5, 9],[6, 3],[7, 9],[8, 1],[8, 2],[8, 5],[8, 7],[9, 0],[9, 1],[9, 6],
]

对比结果如下

这个时候wasm用时居然是js的数倍之多,核心原因就是花费了太多的时间在数据的拷贝和反序列化

rust 复制代码
// rust 的 wasm wrapper
#[wasm_bindgen]
pub fn graham_scan_wasm(array: Vec<JsValue>) -> Vec<JsValue> {
    if array.len() <= 3 {
        return array;
    }

    let array: Vec<Vector> = array
        .into_iter()
        .map(|v| from_value::<[f64; 2]>(v).unwrap())
        .map(|v| Vector { x: v[0], y: v[1] })
        .collect();

    graham_scan(array)
        .into_iter()
        .map(|v| to_value(&v).unwrap())
        .collect()
}
javascript 复制代码
// js 也需要做转换
data.map((v) => ({ x: v[0], y: v[1] }))

或许这样对比对 wasm 不公平

但是wasm本身不具备操作dom的能力,几乎所有的输入数据(用户交互产生的数据,网络请求的数据)都是要从js那边拷贝过来,然后在wasm里面计算,最后再拷贝给js展现, 这才是现实场景的应用,所以数据的拷贝在所难免,一不小心就会导致wasm变慢,这也是目前wasm的一个问题所在

如果大家有什么想法,欢迎在评论区友好讨论。谢谢大家

相关推荐
泉崎几秒前
11.7比赛总结
数据结构·算法
你好helloworld2 分钟前
滑动窗口最大值
数据结构·算法·leetcode
小远yyds20 分钟前
前端Web用户 token 持久化
开发语言·前端·javascript·vue.js
AI街潜水的八角42 分钟前
基于C++的决策树C4.5机器学习算法(不调包)
c++·算法·决策树·机器学习
白榆maple1 小时前
(蓝桥杯C/C++)——基础算法(下)
算法
阿伟来咯~1 小时前
记录学习react的一些内容
javascript·学习·react.js
JSU_曾是此间年少1 小时前
数据结构——线性表与链表
数据结构·c++·算法
吕彬-前端1 小时前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱1 小时前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
guai_guai_guai1 小时前
uniapp
前端·javascript·vue.js·uni-app