微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

为什么 VecDeque 比 Vec 慢?

如何解决为什么 VecDeque 比 Vec 慢?

我开始优化 crate 的性能,我将 Vec 换成了 VecDeque。容器按排序顺序维护元素(它应该相当小,所以我还没有尝试使用堆)并且偶尔会从中间拆分为两个单独的容器(我还没有尝试使用堆的另一个原因) drain

我希望第二个操作更快:我可以复制集合的前半部分,然后简单地旋转并减少原始(现在是第二个)集合的长度。但是,当我运行我的 #[bench] 测试时,以可变次数执行上述操作(低于百万纳秒/迭代),我观察到 VecDeque性能下降 :

测试一个 测试b 测试c 测试d
vec 12.6 5.9 5.9 3.8
VecDeque 13.6 8.9 7.3 5.8

可重现的示例 (gist):

#![feature(test)]
extern crate test;

use std::collections::VecDeque;

fn insert_in_sorted_order_vec<E: Ord + Eq>(t: &mut Vec<E>,k: E) {
    match t.binary_search(&k) {
        Ok(i) => t[i] = k,Err(i) => t.insert(i,k),}
}

fn insert_in_sorted_order_vecdeque<E: Ord + Eq>(t: &mut VecDeque<E>,}
}

fn split_vec<T>(mut t: Vec<T>) -> (Vec<T>,Vec<T>) {
    let a = t.drain(0..(t.len() / 2)).collect();
    (a,t)
}

fn split_vecdeque<T>(mut t: VecDeque<T>) -> (VecDeque<T>,VecDeque<T>) {
    let a = t.drain(0..(t.len() / 2)).collect();
    (a,t)
}

#[cfg(test)]
mod tests {
    use super::*;
    use test::Bencher;

    static ITERS_BEFORE_SPLIT: u32 = 50;
    static ITERS_TIME: u32 = 10_000;

    #[bench]
    fn vec_manip(b: &mut Bencher) {
        b.iter(|| {
            let mut v = Vec::new();
            for i in 0..(ITERS_TIME / ITERS_BEFORE_SPLIT) {
                for j in 1..(ITERS_BEFORE_SPLIT + 1) {
                    insert_in_sorted_order_vec(&mut v,i * j / (i + j)); // 'random'-ish illustrative number
                }

                v = split_vec(v).1;
            }
        });
    }

    #[bench]
    fn vecdeque_manip(b: &mut Bencher) {
        b.iter(|| {
            let mut v = VecDeque::new();
            for i in 0..(ITERS_TIME / ITERS_BEFORE_SPLIT) {
                for j in 1..(ITERS_BEFORE_SPLIT + 1) {
                    insert_in_sorted_order_vecdeque(&mut v,i * j / (i + j)); // 'random'-ish illustrative number
                }

                v = split_vecdeque(v).1;
            }
        });
    }
}

Vec 实现花费了 69.2k ns/iter,而 VecDeque 实现花费了 91.8k。

我已经多次重复并验证了这些结果 - 为什么使用这种更灵活的数据结构会降低性能

这些结果是通过运行 cargo bench 获得的。

  • Linux 5.11
  • 3900X(12 核,3.8-4.6 GHz)
  • 32GB 3200 MHz 内存
  • 每晚 rustc 1.55.0
  • cargo bench 选项(已优化,据我所知没有调试符号等)

编辑

我更改了 split_vecdeque 方法以使用 split_off 而不是 drain().collect()(见下文)。 看起来这个方法保证不会重新分配或移动任何东西,而只是移动headtail指针;请参阅 documentationimplementation。然而,它的性能甚至比 98.2k ns/iter 的原始 VecDeque 还要差。对于较大的值 (ITERS_BEFORE_SPLIT = 50_000,ITERS_TIME = 5_000_000),虽然性能 (21.8m ns/iter) 比 drain (23.1 ns/iter) 好,但比 Vec (19.1 ns/iter) 差.

fn split_vecdeque<T>(mut t: VecDeque<T>) -> (VecDeque<T>,VecDeque<T>) {
    let a = t.split_off(t.len() / 2);
    (t,a)
}

解决方法

A VecDeque 类似于 Vec,但支持从两端有效地推送和弹出。为此,它使用单个连续缓冲区(就像 Vec 一样),但将其视为两个分区;一个头一个尾。

结构如下:

pub struct VecDeque<T> {
    tail: usize,head: usize,buf: RawVec<T>,}

缓冲区中的项目是这样排序的:

[[tail: 5,6,7] ...unused... [head: 1,2,3,4]] 

在集合的末尾添加一个项目将附加到尾部,使用一些未使用的空间。添加到集合的开头将添加到头部的开头,进入相同的空间。当头部和尾部在中间相遇时,VecDeque 已满,需要重新分配。

Vec相比:

pub struct Vec<T> {
    buf: RawVec<T>,len: usize,}

像这样使用它的缓冲区:

[1,4,5,7 ...unused...] 

在最后添加一个项目很快,但在开始添加一个项目需要复制所有现有项目以腾出空间。

VecDeque 上的大多数操作都因这种布局而变得更加复杂,这会略微降低其性能。甚至检索它的长度也更复杂:

pub fn len(&self) -> usize {
    count(self.tail,self.head,self.cap())
}

VecDeque 的全部意义在于使某些操作更快,即推送和弹出集合的开始Vec 在这方面非常慢,尤其是在有很多项目的情况下,因为它涉及移动所有其他项目以腾出空间。 VecDeque 的结构使这些操作速度更快,但与 Vec 相比以牺牲其他操作的性能为代价。

您的测试似乎没有利用 VecDeque 的设计,因为它们主要是调用 insert,这在这两种情况下都涉及对许多项目的昂贵复制。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其他元素将获得点击?
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。)
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbcDriver发生异常。为什么?
这是用Java进行XML解析的最佳库。
Java的PriorityQueue的内置迭代器不会以任何特定顺序遍历数据结构。为什么?
如何在Java中聆听按键时移动图像。
Java“Program to an interface”。这是什么意思?