std::vector 与 std::list 与 std::slist 的相对性能?

Posted

技术标签:

【中文标题】std::vector 与 std::list 与 std::slist 的相对性能?【英文标题】:Relative performance of std::vector vs. std::list vs. std::slist? 【发布时间】:2010-09-19 06:37:29 【问题描述】:

对于不需要随机访问列表元素的简单链表,使用std::list 代替std::vector 是否有任何显着优势(性能或其他方面)?如果需要向后遍历,在迭代其元素之前使用 std::slistreverse() 列表会更有效吗?

【问题讨论】:

(迟到的评论,仅供记录):链接到 Bjarne Stroustrup 的讲座:“为什么你应该避免使用链接列表”,他声称向量总是比列表更好。他给出的原因是平均而言,对插入点的搜索占主导地位,而元素的移动(在向量中)相比之下是微不足道的。另外:缓存未命中,但这已经在下面的答案中提到了。视频:youtube.com/watch?v=YQs6IC-vgmo 尽管 list 几乎总是比 vector 慢(除了在进行高级拼接时),但有一次你绝对需要一个 list:稳定的迭代器。如果您需要保留始终指向同一元素的迭代器的副本(除非已删除),则这不是保证向量可以提供的(例如,对 push_back 的调用可以使所有迭代器无效)。使用内存池可以使列表的速度更接近向量的速度。 【参考方案1】:

像往常一样,性能问题的最佳答案是 profile 两种实现为您的用例,看看哪个更快。

一般而言,如果您在数据结构中插入(除了末尾之外),那么 vector 可能会更慢,否则在大多数情况下,如果仅针对 @987654322,vector 的性能预计会比 list 更好@,这意味着如果数据集中相邻的两个元素在内存中是相邻的,那么下一个元素将已经在处理器的缓存中,并且不必将内存分页到缓存中。

还请记住,vector 的空间开销是恒定的(3 个指针),而 list 的空间开销是为每个元素支付的,这也减少了完整元素的数量(数据加上开销)可以随时驻留在缓存中。

【讨论】:

还要记住,如果必须搜索要插入的位置,即使在向量中插入也比在链表中更快。例如,取一堆随机整数并将它们按排序顺序插入到向量或链表中——无论项目总数如何,向量总是更快,因为在搜索插入点时缓存未命中链表。 几乎唯一一个链表更快的地方是当你进行大量拼接时,因为这不涉及大量缓存未命中,并且每个拼接都是一个常数时间操作(可以将大量项目从一个链表移动到另一个链表)。 "还要记住,向量的空间开销是恒定的" 仅当您非常小心时。对于随意使用,它具有线性空间开销,与list 相同。请参阅:摊销线性 push_back。 @MooingDuck 你是对的,在最坏的情况下vector 将分配两倍于它需要的空间,但除了这个空间的一个恒定部分之外,所有空间都是 cold 和不会导致任何额外的缓存命中。【参考方案2】:

在 C++ 中要考虑的默认数据结构是 Vector

考虑以下几点...

1] 遍历: 列表节点分散在内存中的任何地方,因此列表遍历会导致缓存未命中。但是向量的遍历是平滑的。

2] 插入和删除: 当您对 Vector 执行此操作时,平均 50% 的元素必须移动,但缓存非常擅长此操作!但是,对于列表,您需要遍历到插入/删除点...所以再次...缓存未命中! 令人惊讶的是,向量也赢得了这场官司!

3] 存储: 当您使用列表时,每个元素有 2 个指针(向前和向后),因此列表比向量大得多! 向量只需要比实际元素多一点的内存。 你应该有理由不使用向量。


参考: 我在 Bjarne Stroustrup 的一次谈话中了解到这一点:https://youtu.be/0iWb_qi2-uI?t=2680

【讨论】:

我认为您的意思是缓存未命中,但作为独立游戏开发者,我编写的代码也有一些现金未命中。 java.dzone.com/articles/c-benchmark-%E2%80%93-stdvector-vs 有点像 Bjarne 所说的,但测试的数字和源代码更好。 @gulgi ,该链接值得单独回答,而不仅仅是评论。如果在 *** 上有图表和简短的解释,那就太好了。【参考方案3】:

根本没有。 List 比 Vector 有优势,但顺序访问不是其中之一 - 如果这就是你所做的一切,那么 vector 会更好。

但是.. 添加额外元素的向量比列表更昂贵,尤其是在中间插入时。

了解这些集合是如何实现的:向量是数据的顺序数组,列表是包含数据和指向下一个元素的指针的元素。一旦你理解了这一点,你就会明白为什么列表对插入有利,而对随机访问不利。

(所以,向量的反向迭代和前向迭代完全一样——迭代器每次只是减去数据项的大小,列表仍然要通过指针跳转到下一项)

【讨论】:

这是显而易见的,而且 99% 的时间都是正确的答案。如果您需要向后遍历,请向后运行您的 for 循环。数组/向量提供随机访问、非常快速的顺序访问以及从向量中任何随机起点开始的同样快速的顺序访问。喜欢的列表只有一个长处,那就是删除成员或在列表中的某个位置插入成员。他们几乎不擅长其他所有事情。慢,慢,慢。增长一个数组/向量就像一个新的 malloc() 和 memmove() 一样简单。使用 Vprime、Vgrow,您可以重新分配它们并来回复制。【参考方案4】:

如果您需要向后遍历,则 slist 不太可能成为您的数据结构。

传统的(双向)链表为您在列表中的任何位置提供恒定的插入和删除时间;向量仅在列表末尾提供摊销的常数时间插入和删除。对于向量的插入和删除时间,除了末端以外的任何地方都是线性的。这不是全部。也有恒定的因素。向量是一种更简单的数据结构,其优点和缺点取决于上下文。

了解这一点的最佳方法是了解它们是如何实现的。链表的每个元素都有一个 next 和一个 previous 指针。向量具有由索引寻址的元素数组。从中可以看出两者都可以进行高效的前向和后向遍历,而只有一个向量才能提供高效的随机访问。您还可以看到,链表的内存开销是每个元素的,而向量的内存开销是恒定的。您还可以看到为什么两个结构之间的插入时间不同。

【讨论】:

重要的一点是,列表的每个元素都有额外的指针,而向量只有三个指针用于整个数据结构。 是的,这是真的:我的回答是:“链表的每个元素都有一个下一个前一个指针。向量有一个由索引寻址的元素数组。”很明显,“and”这个词不见了(!),我会把额外的指针弄清楚。【参考方案5】:

关于该主题的一些严格基准: http://baptiste-wicht.com/posts/2012/12/cpp-benchmark-vector-list-deque.html

正如其他人所指出的,连续的内存存储意味着 std::vector 对大多数事情来说更好。除了少量数据(可以全部放入缓存中)和/或经常擦除和重新插入之外,几乎没有使用 std::list 的充分理由。 由于缓存和主内存速度 (200x) 之间的差异以及连续内存访问如何影响缓存使用,复杂性保证与实际性能无关。请参阅 Chandler Carruth (google) 在此处讨论该问题: https://www.youtube.com/watch?v=fHNmRkzxHWs

还有 Mike Acton 的面向数据设计的演讲: https://www.youtube.com/watch?v=rX0ItVEVjHc

【讨论】:

【参考方案6】:

有关费用的详细信息,请参阅此问题:What are the complexity Guarantees of the standard containers

如果您有一个 slist,并且现在想以相反的顺序遍历它,为什么不将类型更改为到处列出呢?

【讨论】:

【参考方案7】: std::vector 查找元素的速度比 std::list 快得多 std::vector 在数据非常少的情况下总是比 std::list 执行得更快 std::vector 在后面推元素总是比 std::list 快 std::list 可以很好地处理大元素,尤其是对于 在前面排序或插入

注意:如果您想了解更多关于性能的信息,我建议您查看this

【讨论】:

以上是关于std::vector 与 std::list 与 std::slist 的相对性能?的主要内容,如果未能解决你的问题,请参考以下文章

为啥从 std::vector 中随机删除比 std::list 快?

指向std :: vector和std :: list元素的指针

指向 std::vector 和 std::list 元素的指针

std::vector 或 std::list 用于 std::unordered_map 存储桶?

使用std::vector优化点云动画显示一例

使用初始化列表作为值时插入 std::list<std::vector<int>> 失败?