• 作者:老汪软件技巧
  • 发表时间:2024-09-01 17:03
  • 浏览量:

回顾

开始新内容前让我们先回忆下之前的内容,之前我们构造了一棵约30万个节点的树,测出来结果 wasm比js慢约4倍,比较反直觉,所以我们拆分成:循环、对象深拷贝、向量切片、数学函数、向量的插入和删除,对这几部分分别测试,看看到底哪里慢了。

我们已经测试了循环、深拷贝、向量切片及原始指针、向量尾部操作、数学函数,对比结果是 wasm快。这篇内容我们测试下向量的头尾操作,和js对比看谁快。

构造向量的头尾操作

rust代码

#[wasm_bindgen]
#[derive(Clone)]
pub struct Item {
    name: String,
    children: Vec,
}
#[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
        const item = {
            name: '',
            children: [],
        };
        nodeStack.push(item);
    }
    for (let i=0; i
        nodeStack.shift();
    }
}

结论

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

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

wasm这么慢,虽然push可能触发向量扩容及内存拷贝(参考向量大小与容量),但发现push很快,确定是向量头部删除很慢。

都是数组头部删除,那为什么js的就快很多,这里解开谜底:

向量平行公式和垂直公式__向量垂直公式

rust的向量是一个动态数组,和大多数语言的数组概念类似,都是开辟一片连续内存,且元素类型都相同,这样做是为了高效访问,缺点是头部操作效率低,牵扯到后面每个元素的移动。

js的数组概念就不一样了,为了追求灵活方便,数组里可以放不同类型的数据,且要支持稀疏性和非整数键,有时类似一个哈希表、有时又是一个真正的数组(一片连续内存),即使底层是真正的数组,V8也对数组头部删除做了优化,只做删除标记延迟做元素移动。

对比来看js的数组和rust的向量不是一个东西,js对底层数据结构做了更多的封装及优化,追求的是灵活方便,但开发者很难参与到底层性能优化。

优化

既然数组这种数据结构不适合头部操作,分析下代码发现构造树的整个逻辑,所有向量操作也只发生在头尾,所以我们想到了双端队列 VecDeque,换成这个试试。

rust代码

#[wasm_bindgen]
pub fn vecDeqFun(len: usize) {
    let mut nodeStack: VecDeque = 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比js快约12倍。

问题找到了,那让我们重构下最开始反直觉的那段代码,看看效果如何。

欲知后事如何且听下回分解。