Skip to content
风起
风起
2023-06-28

wasm VS js,谁快?

我们做一个测试,分别用wasm和js构建同一棵树,实现算法也相同,来对比下谁更快,初步了解下wasm的性能,这里用rust编译wasm。

我们构造的树结构如下

{
	name: "node_0_0",
	children: [
		{
			name: "node_1_0",
			children: [...],
		},
		{
			name: "node_1_1",
			children: [...],
		},
		...
	],
}

第一次尝试:

rust代码

#[wasm_bindgen]
#[derive(Clone)]
pub struct Item {
    name: String,
    children: Vec<Item>,
}

#[wasm_bindgen]
pub fn treeFun(treeLevel: u32, nodeWidth: u32) -> Item {
    let tempNode = Item {
        name: String::from(""),
        children: vec![],
    };

    let mut root = tempNode.clone();
    root.name = String::from("node_0_0");

    if treeLevel < 2 {
        return root;
    }

    let mut nodeStack: Vec<&mut [Item]> = vec![];

    for m1 in 0..nodeWidth {
        let mut curItem = tempNode.clone();
        curItem.name = format!("node_{}_{}", 1, m1);
        root.children.push(curItem);
    }

    let mut vs: Vec<&mut [Item]> = root.children.chunks_mut(1).collect();
    for m2 in vs.iter_mut() {
        nodeStack.push(*m2);
    }

    for i in 2..treeLevel {
        let curTotal: u32 = nodeWidth.pow(i - 1);

        for j in 0..curTotal {
            let shiftItem = nodeStack.remove(0);

            for k1 in 0..nodeWidth {
                let mut curItem = tempNode.clone();
                curItem.name = format!("node_{}_{}", i, nodeWidth * j + k1);
                shiftItem[0].children.push(curItem);
            }

            let mut ss: Vec<&mut [Item]> = shiftItem[0].children.chunks_mut(1).collect();
            for _ in 0..nodeWidth {
                let curSs = ss.remove(0);
                nodeStack.push(curSs);
            }
        }
    }

    root
}

js代码

const treeFun = (treeLevel, nodeWidth) => {
	const tempNode = {
    	name: '',
    	children: [],
	};
	
    const root = JSON.parse(JSON.stringify(tempNode));
    root.name = 'node_0_0';

    if (treeLevel < 2) {
        return root;
    }

    const nodeStack = [];

    for (var m1=0; m1<nodeWidth; m1++) {
        const curItem = JSON.parse(JSON.stringify(tempNode));
        curItem.name = `node_1_${m1}`;
        root.children.push(curItem);
    }

    for (var m2=0; m2<nodeWidth; m2++) {
        nodeStack.push(root.children[m2]);
    }

    for (var i=2; i<treeLevel; i++) {
        const curTotal = Math.pow(nodeWidth, (i-1));

        for (var j=0; j<curTotal; j++) {
            const shiftItem = nodeStack.shift();

            for (var k1=0; k1<nodeWidth; k1++) {
                const curItem = JSON.parse(JSON.stringify(tempNode));
                curItem.name = `node_${i}_${nodeWidth * j + k1}`;
                shiftItem.children.push(curItem);
            }

            for (var k2=0; k2<nodeWidth; k2++) {
                nodeStack.push(shiftItem.children[k2]);
            }
        }
    }

    return root;
}

我们尝试构造的树有7层深度,每个节点下有8个子节点,共299593个节点,执行结果如下

total nodes:  299593
wasm-tree: 31134.56591796875 ms
js-tree: 7428.5888671875 ms

可以看到js执行速度明显快,奇怪了,与预期不符。

分析rust代码,发现这里的逻辑主要包含: 循环、对象深拷贝、向量的切片、数学函数、向量的插入和删除等几部分。

我们按照这几块分别测试,看看到底哪里慢了。

第二次尝试:循环性能对比

我们构造4层嵌套的循环

rust代码

#[wasm_bindgen]
pub fn forFun(len: usize) -> usize {
    let mut total = 0;

    for i in 0..len {
        total += i;
        for j in 0..len {
            total += j;
            for m in 0..len {
                total += m;
                for n in 0..len {
                    total += n;
                }
            }
        }
    }

    total
}

js代码

\
const forFun = (len) => {
    let total = 0;
    for (let i=0; i<len; i++) {
        total += i;
        for (let j=0; j<len; j++) {
            total += j;
            for (let m=0; m<len; m++) {
                total += m;
                for (let n=0; n<len; n++) {
                    total += n;
                }
            }
        }
    }
    return total;
}

将len=50传入执行,执行结果

wasm-for: 0.138916015625 ms
js-for: 13.94189453125 ms

可以看到wasm明显快,问题不在这里,继续测试

第三次尝试:对象深拷贝性能对比

我们反复深拷贝对象进行测试

rust代码

#[wasm_bindgen]
#[derive(Clone)]
pub struct Item {
    name: String,
    children: Vec<Item>,
}

#[wasm_bindgen]
pub fn copyFun(len: usize) {
    let mut nodeStack = vec![];
	let tempNode = Item {
        name: String::from(""),
        children: vec![],
    };
    for i in 0..len {
        let mut curItem = tempNode.clone();
        curItem.name = format!("node_{}", i);
        nodeStack.push(curItem);
    }
}

js代码

const copyFun = (len) => {
    const nodeList = [];
    const tempNode = {
    	name: '',
    	children: [],
	};
    for (let i=0; i<len; i++) {
        const curItem = JSON.parse(JSON.stringify(tempNode));
        curItem.name = `node_${i}`;
        nodeList.push(curItem);
    }
}

将len=5000传入执行,执行结果

wasm-copy: 7.875 ms
js-copy: 12.510009765625 ms

可以看到wasm明显快,问题不在这里,继续测试

第四次尝试:向量切片性能对比

我们将向量的每个元素先进行切片,然后将切片存入另一个向量中

注意:

1,rust这里切片完后直接append,没有从ss的头部删一个再加到nodeStack的尾部,排除向量头部删除性能问题

2,由于js没有切片概念,所以rust用另外一种不用切片的unsafe方法,直接取向量原始指针存入nodeStack

rust代码

#[wasm_bindgen]
pub fn sliceFun(len: usize) {
    let mut nodeStack: Vec<&mut [usize]> = vec![];
    let mut arr = vec![1; len];
    let mut ss: Vec<&mut [usize]> = arr.chunks_mut(1).collect();
    nodeStack.append(&mut ss);
}

#[wasm_bindgen]
pub fn unsafeSliceFun(len: usize) {
    let mut nodeStack: Vec<&mut usize> = vec![];
    let mut arr = vec![1; len];
    let ptr = arr.as_mut_ptr();
    unsafe {
        for i in 0..len {
            nodeStack.push(&mut *ptr.add(i));
        }
    }
}

js代码

const sliceFun = (len) => {
    const nodeStack = [];
    const arr = Array(len).fill(1);
    for (let i=0; i<len; i++) {
        nodeStack.push(arr[i]);
    }
}

将len=20000传入执行,执行结果

wasm-slice: 0.5888671875 ms
wasm-unsafe-slice: 0.1650390625 ms
js-slice: 6.85205078125 ms

可以看到无论有么有切片wasm都比js快,当然操作原始指针是最快的。

第五次尝试:数学函数pow性能对比

我们反复执行pow函数

rust代码

#[wasm_bindgen]
pub fn powFun(len: usize) {
    let num: u32 = 10;
    for i in 0..len {
        num.pow(i.try_into().unwrap());
    }
}

js代码

const powFun = (len) => {
    const num = 10;
    for (let i=0; i<len; i++) {
        Math.pow(num, i);
    }
}

将len=7000传入执行,执行结果

wasm-pow: 0.0478515625 ms
js-pow: 1.115234375 ms

还是wasm快,继续测试

第六次尝试:vector性能对比

我们在一个向量里做尾部插入,头部删除来测试

rust代码

#[wasm_bindgen]
#[derive(Clone)]
pub struct Item {
    name: String,
    children: Vec<Item>,
}

#[wasm_bindgen]
pub fn vecFun(len: usize) {
    let mut nodeStack = vec![];
    for _ in 0..len {
        let tempNode = Item {
            name: String::from(""),
            children: vec![],
        };
        nodeStack.push(tempNode);
    }

    for _ in 0..len {
        nodeStack.remove(0);
    }
}

js代码

const vecFun = (len) => {
    const nodeStack = [];
    for (let i=0; i<len; i++) {
        const item = {
            name: '',
            children: [],
        };
        nodeStack.push(item);
    }

    for (let i=0; i<len; i++) {
        nodeStack.shift();
    }
}

将len=7000传入执行,执行结果

wasm-vec: 481.01708984375 ms
js-vec: 6.257080078125 ms

wasm这么慢,这时又测试了下向量的push,虽然push可能触发向量扩容及拷贝元素(参考向量大小与容量),但发现push很快,确定是向量头部删除很慢。

问题找到了,我们现在优化,发下构造树用到的队列只在两头操作,所以想到了双端队列

rust代码

#[wasm_bindgen]
pub fn vecDeqFun(len: usize) {
    let mut nodeStack: VecDeque<Item> = VecDeque::new();
    for _ in 0..len {
        let item = Item {
            name: String::from(""),
            children: vec![],
        };
        nodeStack.push_back(item);
    }

    for _ in 0..len {
        nodeStack.pop_front();
    }
}

再次将len=7000传入执行,执行结果

wasm-vec: 508.14501953125 ms
wasm-vec-deq: 0.658935546875 ms
js-vec: 7.974853515625 ms

可以看到wasm-vec-deq用时最短,即双端队列最适合这种场景

第七次尝试:重构树的构建,性能对比

现在用双端队列及向量原始指针重构树的构建

rust代码

#[wasm_bindgen]
#[derive(Clone)]
pub struct Item {
    name: String,
    children: Vec<Item>,
}

#[wasm_bindgen]
pub fn unsafeTreeFun(treeLevel: u32, nodeWidth: u32) -> Item {
    let tempNode = Item {
        name: String::from(""),
        children: vec![],
    };

    let mut root = tempNode.clone();
    root.name = String::from("node_0_0");

    if treeLevel < 2 {
        return root;
    }

    let mut nodeStack: VecDeque<&mut Item> = VecDeque::new();

    for m1 in 0..nodeWidth {
        let mut curItem = tempNode.clone();
        curItem.name = format!("node_{}_{}", 1, m1);
        root.children.push(curItem);
    }

    let ptr1 = root.children.as_mut_ptr();
    unsafe {
        for m2 in 0..nodeWidth {
            nodeStack.push_back(&mut *ptr1.add(m2.try_into().unwrap()));
        }
    }

    for i in 2..treeLevel {
        let curTotal: u32 = nodeWidth.pow(i - 1);

        for j in 0..curTotal {
            let shiftItem = nodeStack.pop_front().unwrap();

            for k1 in 0..nodeWidth {
                let mut curItem = tempNode.clone();
                curItem.name = format!("node_{}_{}", i, nodeWidth * j + k1);
                shiftItem.children.push(curItem);
            }

            let ptr2 = shiftItem.children.as_mut_ptr();
            unsafe {
                for k2 in 0..nodeWidth {
                    nodeStack.push_back(&mut *ptr2.add(k2.try_into().unwrap()));
                }
            }
        }
    }

    root
}

js代码参考第一次尝试里的,这里没有变。

还是一个7层深度,每个节点下8个子节点,共299593个节点的树,执行结果

total nodes:  299593
wasm-deq-tree: 347.54443359375 ms
js-tree: 7232.81201171875 ms

最终可以看到wasm快的多,对于树的构建速度大概是js的20倍

总结

这里测试只考虑wasm和js的执行速度,排除了wasm和js传递大量数据的场景,只是纯粹的体验下wasm的执行速度。

通过这个测试了解到,wasm从设计机制上确实比js运行得快,如果发现不快就要开始反思,是不是自己代码写的有问题。

至于wasm和js的传参,始终遵循最小化序列化和最小化复制的原则,参考《wasm内存模型》