【问题标题】:Why vectors outperform lists when number of elements is small?为什么当元素数量很少时向量优于列表?
【发布时间】:2026-01-29 08:55:01
【问题描述】:

Bjarne Stroustrup 在他的《The C++ Programming Language (4th Edition)》一书中指出:

请注意,vector 通常是(令人惊讶的是,除非您 了解机器架构)比list 更有效 小元素的短序列(即使是 insert()erase())。

他没有进一步详细说明,所以我想知道为什么它是正确的,以及这些序列大概有多短(即元素的数量)?

【问题讨论】:

  • 查看 Herb Sutter 的演讲:Modern C++: What You Need to Know。它实际上解决了它们必须有多短。 FWIW,这是一张幻灯片:i.imgur.com/XRvjKpi.png
  • 这有点像说“什么更快,O(1) 或 O(n)”。有人说“显然是 O(1)”,但随后你回答“啊哈,所有输入大小的 O(1) 算法需要 30 秒,但 O(n) 算法需要 4n 秒,我们只有 5 个项目时间”。确切的细节将取决于实施。
  • inserterase 不在 vector 的末尾是最坏情况的操作。但是如果向量的大小很小,即使是那些也可以在没有太多工作的情况下执行。简单操作的效率加上缓存一致性使它们比您想象的更快。

标签: c++ vector linked-list


【解决方案1】:

...以及这些序列的大致长度

正确答案是:你必须测量。

这取决于您在容器中存储的内容、构建、复制和移动该类型的成本、它的大小以及您的访问和插入模式。

它也会因机器和可能的编译器而异。


回答你问题的第一部分:

...所以我想知道为什么它是真的...

在这种情况下它是正确的,原因是向量的连续和最小填充的内存布局与现代 CPU 的缓存子系统的协作比列表的更好。

  • vector

    • 您的循环可能类似于

      for (size_t i=0; i < v.size(); ++i) {
      

      其中v.size() 通常只能调用一次,并且i 都可以放入寄存器。如果其他依赖项允许,分支预测可以很好地允许多个连续迭代交错运行,从而保持指令流水线满

    • 您的下一次访问 (v[i+1]) 可能已经在缓存中,因此速度非常快。要么和v[i]在同一个缓存行,要么下一个,顺序读取相对容易预取

    • 无论您的多少值适合一个高速缓存行,这就是您获得的每个高速缓存行的数量。之间没有(不必要的)填充,因此没有不必要的缓存未命中
  • list

    • 您的循环可能类似于

      for (list::const_iterator i=l.begin(); i != l.end(); ++i) {
      

      i 的每个连续值都必须从内存中加载(通过前一个节点的指针)

    • 您的下一次访问 (*++i)可能已经在缓存中,但是:

    • 1234563如果您的值与指针大小相同(我们讨论的是小值),那么使用列表在缓存行中获得三分之一的数据,导致缓存未命中数增加三倍。
  • 另请注意,下一次迭代的推测执行取决于加载值而不是增加寄存器:这里的缓存未命中不仅本身速度较慢,而且还会停止推测执行

【讨论】:

    【解决方案2】:

    我认为这是因为列表使用动态方法来扩展列表(可能是指向下一个列表的指针,以便您可以轻松地插入特定位置而无需重新排列内存)。而向量实际上是一个简单的数组,带有一些漂亮的糖衣以使其更易于使用(如果您使用正确的函数,则边界问题有可选的例外)。

    基本上,当你实例化一个向量时,它会分配一个 T 类型的小数组。当你最后推送一个新项目时,它只是将值写入已经分配的空间,如果你还没有变得太大的话.但是,当您添加到列表时,它必须分配内存并将该位置存储在各种指针中(我在这里猜测,但这就是链表的工作方式)。如果您知道向量有多大(或者可以合理猜测),那么它非常有效。但是,一旦超过了它的底层数组的大小,它就必须为新扩大的数据结构重新分配内存,然后复制所有内容。这对于将向量增长到大尺寸是非常昂贵的。在我的脑海中,向量呈指数增长,因此在几次失败后故障数量会减少,但它仍然非常昂贵。

    【讨论】:

    • 您认为向量更快是因为列表不必重新排列内存?