【问题标题】:Fastest insert C++最快的插入 C++
【发布时间】:2014-01-14 02:39:56
【问题描述】:

鉴于数组/向量非常大,有什么方法可以非常快地在数组/向量的任何位置插入数据?

如果我使用 vector::insert 向量会将所有项目移动到我的项目之后,这将花费很多时间,例如,如果向量有 1b 个项目并且在向量中间执行此操作,则向量将移动500m 项。

是否有任何有效的方法可以使用 C 样式/C++ 数组或向量来执行此操作?

【问题讨论】:

  • 如果这是性能问题,您可能会开始寻找不同的容器。
  • 数组的大小是固定的,所以你不能真正插入元素。
  • 一个向量中的 5 亿个项目无论如何都在推动它。您需要相当多的 RAM 才能使其可行,除非它们都是某种基本类型。
  • @cHao 我没发现问题。完全没有。我在内存中处理过更大的数据集。 (也:很好的双关语:))
  • @sehe 他说可能是什么问题 - RAM。每 1 个字节的元素大小需要大约 500MB RAM。实际上,OP 说的是 1b 个项目,这意味着每 1 个字节的元素大小有 1GB RAM。这绝对不是微不足道的。

标签: c++ arrays vector


【解决方案1】:

理论(特别是大 O 表示法)认为,链表的插入和删除复杂度为 O(1),而动态数组的复杂度为 O(n),这是因为擦除/插入需要容器移动。

但这就是理论。计算机不仅仅是理论。在实践中,它大不相同

现代计算机将它们的内存排列在所谓的内存层次中,即一组按速度排序的不同类型的内存设备:

+---------------+
| CPU registers |   ^
+---------------+   |
|   L1 cache    |   |
|      ...      |   |   Less apacity
|   LN cache    |   |   Faster access
+---------------+   |   More expensive hardware
|      RAM      |   |
+---------------+   
|      HDD      |
+---------------+

如图所示,层次结构是通过内存访问的速度来组织的。但请注意,更高的速度意味着更昂贵的硬件,因此这意味着许多慢速访问内存和一些快速内存
因此,提高程序性能的一种方法是将频繁使用的数据保存在快速内存中,并且仅在必要时才进入慢速内存(程序请求的数据未加载到快速内存中,例如示例)。

这正是硬件所做的,假设有两种行为:

  • 如果请求了一条数据,未来可能会请求它附近的数据。这称为Locality of reference。例如,考虑一下我们如何使用数组。
  • 如果请求了一条数据,将来可能会再次请求它。这就是所谓的时间局部性。同样,一次又一次地迭代和数组就是一个例子。

当然内存是有限的,所以一个新数据的请求将被加载到层次结构的某个级别,会丢弃之前存在的数据。

那么,为什么这对不同容器的性能很重要?

记住链表(std::list 是链表)是如何工作的:
链表是通过指针在它们之间连接的分离节点链:

+---+     +---+             +---+
| 1 | --> | 2 | --> ... --> | N |
+---+     +---+             +---+

另一方面,动态数组(std::vector 是动态数组)是一块连续的内存

+---+---+-----+---+
| 1 | 2 | ... | N |
+---+---+-----+---+

正如我上面所说的,理论认为链表插入/删除具有 O(1) 复杂性,因为“只是改变指针”。但是考虑一下如何访问内存来做到这一点。 您是否注意到该过程不满足空间局部性规则?。所以这有很多未命中(缓存未命中),即从快速内存中请求新的内存,性能下降。
事实上,即使理论上说遍历链表具有 O(n) 复杂度,但在实践中,由于缓存未命中而导致的性能持续下降。

现在考虑动态数组是如何工作的:插入/删除确实具有 O(n) 复杂性,因为您必须将数组的一侧移动一个位置以便为新元素留出间隙,或者在以下情况下消除该间隙您正在擦除。
但是请记住,数组是一个连续的内存块,因此如果您正在使用它,可能该数组已完全(或几乎部分)加载到快速内存中(sapce-locality),所以转换过程非常快

如您所见,在现代架构中,动态数组比链表快得多

总的来说,std::vector 相对于std::list 有很多优势:

  • 它的缓存友好。正如我们所讨论的,std::list 不是,“缓存友好性”对于当今的性能非常重要。这导致快速插入/移除、快速随机访问和快速复制。
  • 链表在每次插入/删除时执行一次动态内存操作。调用malloc()/free()(即调用操作系统以检索/离开堆内存)需要很多时间。另一方面,动态数组仅以 O(logn) 的平均值进行取消/分配。

但这还不是全部:在少数情况下,std::list 必须是首选,复制/移动元素的成本非常高std::vector 性能的要点是基于缓存中完成的廉价移位过程,但如果向量的元素具有某些昂贵的复制/移动语义,则根本没有任何好处,列表的性能优于向量

阅读this article了解有关该主题的更多信息。

【讨论】:

  • “链表的插入和删除复杂度为 O(1)”只有在您已经知道要插入/删除的位置时才是正确的。如果您首先必须find 那个位置,那么您将回到 O(n)。
  • @Sjoerd 这就是重点。我写了这篇文章是为了澄清,即使没有查找过程,向量 chache 策略的性能也比列表好得多。当然,如果你必须找到插入点,那就更糟了。
  • 您总是可以覆盖分配器以从大块中分割块并确保列表节点的局部性。
  • @Raja 当然。您可以改进定制它的容器的行为(在这种情况下通过分配器)。但我说的是容器原样。此外,如果您需要修改容器,则表明您需要其他容器。
【解决方案2】:

C++ 向量实际上是具有智能调整大小的数组,如果您需要在容器的前面插入,则需要考虑其他东西,例如链表。但是,请记住,如果性能对您非常重要,那么这些其他容器通常在内存中不连续,因此当您遍历它们并在内存中跳转时,通常会导致内存分页问题。这总是一种权衡。

【讨论】:

  • 这里不建议使用链表。它会增加相当大的开销并可能使系统爬行(std::list 甚至允许在 c++11 之前对size() 进行 O(n) 实现...)
  • 你能解释一下这个内存分页的事情吗?我可以以某种方式禁用它吗?
  • 我可能会先查看std::deque,然后再查看std::list 是否插入到前面。
  • @Luka:UNIX 上的内存映射和 mlock(),Windows 上的 VirtualLock。请记住,无论如何,对于任何类型的线性顺序访问,参考的降级局部性都会降低成本。 (PS。使用例如 Boost IPC 分配器和容器类来定位内存映射区域中的数据)
  • @Luka,不,你不能,这就是数据结构的实现方式——每个元素都在堆上分配,并且可以在内存中的任何位置结束,所以当你迭代时,你可能会跳来跳去;另一方面,数组保证是连续的,因此迭代非常快。您的应用程序还对数据做了什么?
猜你喜欢
  • 1970-01-01
  • 2016-09-09
  • 2012-01-31
  • 2018-06-29
  • 1970-01-01
  • 1970-01-01
  • 2016-08-12
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多