网上各种介绍wasm
的文章都在说wasm
的性能是js
的数倍之多,实际情况真的是这样吗?
本文用rust
编译到wasm
与js
进行性能对比,尝试解答这个问题
普通计算
直接上代码,如果是计算斐波那契数列
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
的数倍之多, 但是由此就能得到结论wasm
比js
快吗?
现实场景
考虑一个真实的场景,计算凸包, 因为笔者工作原因,最近有用到该算法,就拿该算法来对比
本文不讨论该算法的细节,后面会出一篇文章专门来介绍该算法的实现
编写一个 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
的一个问题所在
如果大家有什么想法,欢迎在评论区友好讨论。谢谢大家