【问题标题】:Array Performance very similar to LinkedList - What gives?数组性能与 LinkedList 非常相似 - 是什么?
【发布时间】:2011-04-19 08:05:10
【问题描述】:

所以标题有点误导......我会保持简单:我正在比较这两种数据结构:

  1. 一个数组,它从大小 1 开始,对于每个后续添加,都会调用 realloc() 来扩展内存,然后将新的(分配的)元素附加到 n-1 位置。
  2. 一个链表,用来跟踪头部、尾部和大小。并且添加涉及分配新元素并更新尾指针和大小。

不要担心这些数据结构的任何其他细节。这是我在此测试中唯一关心的功能。

理论上,LL 应该表现更好。但是,它们在涉及 10、100、1000... 最多 5,000,000 个元素的时间测试中几乎相同。

我的直觉是堆很大。我认为 Redhat 上的数据段默认为 10 MB?我可能是错的。无论如何,realloc() 首先检查在已分配的连续内存位置 (0-[n-1]) 的末尾是否有可用空间。如果第 n 个位置可用,则不存在元素的重定位。相反, realloc() 只保留旧空间 + 紧随其后的空间。我很难找到这方面的证据,而且我也很难证明这个阵列在实践中的性能应该比 LL 差。

在阅读以下帖子后,这里有一些进一步的分析:

[更新 #1] 我已经修改了代码,使其具有一个单独的列表,该列表在 LL 和 Array 的每 50 次迭代中分配一次内存。对于阵列中的 100 万个添加,几乎始终有 18 个移动。 LL没有搬家的概念。我做了一个时间比较,它们仍然几乎相同。以下是增加 1000 万次的一些输出:

(Array)
time ./a.out a 10,000,000
real    0m31.266s
user    0m4.482s
sys     0m1.493s
(LL)
time ./a.out l 10,000,000
real    0m31.057s
user    0m4.696s
sys     0m1.297s

我预计 18 步的时间会大不相同。数组加法需要 1 次赋值和 1 次比较来获取并检查 realloc 的返回值,以确保发生移动。

[更新 #2] 我在上面发布的测试中运行了 ltrace,我认为这是一个有趣的结果......看起来 realloc(或某些内存管理器)正在根据当前大小抢先将数组移动到更大的连续位置。 对于 500 次迭代,在迭代中触发了内存移动: 1、2、4、7、11、18、28、43、66、101、154、235、358 这非常接近求和序列。我觉得这很有趣 - 我想我会发布它。

【问题讨论】:

  • 你可以很容易地找到 realloc() 扩展块而不是移动它的证据 - 如果指针 realloc() 返回的指针与你传递它的指针相同,那么它扩展了块。跨度>
  • 如果您想知道realloc 是否真的需要复制数据,只需输入一些代码来比较调用前后的指针。它们不同的时间是它复制的时间。每次复制时将数组大小加起来,您会看到数组有多少开销。
  • 有些系统每次需要重新分配时都会将内存分配乘以 2(或大约);这大大减少了操作时间。

标签: c arrays performance unix linked-list


【解决方案1】:

您认为链表应该在最后插入时表现更好的理论的基础是什么?由于您所说的原因,我不希望它发生。 realloc 只会在必须保持连续性时复制;在其他情况下,它可能必须合并空闲块和/或增加块大小。

但是,每个链表节点都需要重新分配和(假设是双链表)两次写入。如果您想了解realloc 的工作原理,只需比较realloc 之前和之后的指针即可。你应该发现它通常不会改变。

我怀疑由于您为每个元素调用realloc(在生产中显然不明智),realloc/malloc 调用本身是这两个测试的最大瓶颈,尽管realloc 通常不会'不提供新的指针。

另外,您混淆了堆和数据段。堆是内存分配的地方。数据段用于全局和静态变量。

【讨论】:

  • "realloc 只会在必要时复制,而每个链表节点都需要分配和(假设双链表)两次写入。" - 您忘记了 realloc 仍将为新条目分配空间。它仍然必须检查堆并在数组之后保留 n+1 空间(假设没有发生移动)。
  • @Jmoney,我已经澄清了。您说得对,它可能必须更新簿记信息。不过,这并不总是正确的。例如。如果你malloc 10 个字节,它可能会给你一个 16 字节的块。如果你realloc到11个字节,就不需要做任何事情了。
【解决方案2】:

(已更新。) 正如其他人所指出的,如果重新分配之间没有其他分配,则不需要复制。同样正如其他人所指出的那样,对于比页面还小的非常小的块,内存复制的风险会降低(当然也会降低其影响)。

此外,如果您在测试中所做的只是分配新的内存空间,那么您发现几乎没有什么不同我并不感到惊讶,因为分配内存的系统调用可能会占用大部分时间。

相反,请根据您希望如何实际使用它们来选择您的数据结构。例如,帧缓冲区可能最好由连续数组表示。

如果您必须在结构中快速重新组织或排序数据,则链表可能会更好。

然后这些操作的效率或多或少取决于你想要做什么。

(感谢下面的 cmets,我最初对这些东西的工作原理感到困惑。)

【讨论】:

  • 好答案;你能提供一些引用吗?
  • 我不知道您为什么认为过度提交意味着没有复制。每个进程仍然只有一个虚拟地址空间,如果内存不能连续扩展,就必须复制。
  • realloc 在实际场景中会经常复制数据,只是因为你可能会在其间分配其他对象,并且地址空间不再连续。在您只重新分配一个数组而不执行其他 malloc 的测试场景中,内存片可能只是被扩展。不过,这与过度使用无关。
  • @Matthew,我想你是对的,关于 Linux。如果其他指针已经占用了您要扩展(重新分配)指针所在的空间,那么是的,必须复制数据。
  • OpenBSD 也是如此。保护页的唯一目的是检测某些无效的内存访问。 realloc 仍会在需要时复制。
【解决方案3】:

使用realloc() 扩展的基于数组的解决方案的性能将取决于您创建更多空间的策略。

如果您通过在每次重新分配时添加固定数量的存储来增加空间量,您最终会得到一个扩展,该扩展平均取决于您存储在数组中的元素数量。这是基于 realloc 需要(偶尔)在别处分配空间并复制内容的假设,而不是仅仅扩展现有分配。

如果您通过添加当前元素数量的一部分来增加空间量(加倍是相当标准的),您最终会得到一个平均需要恒定时间的扩展。

【讨论】:

    【解决方案4】:

    这不是现实生活中的情况。据推测,在现实生活中,您有兴趣查看甚至从数据结构中删除项目以及添加它们。

    如果您允许删除,但仅从头部删除,则链表会比数组更好,因为删除一个项目是微不足道的,如果您不释放已删除的项目,而是将其放在一个空闲列表中以进行回收,您当您将项目添加到列表时,可以消除大量所需的 malloc。

    另一方面,如果您需要随机访问结构,显然数组胜过链表。

    【讨论】:

      【解决方案5】:

      假设你的链表是一个指向第一个元素的指针,如果你想在最后添加一个元素,你必须先遍历链表。这是O(n) 操作。

      假设realloc 必须将数组移动到新位置,它必须遍历数组来复制它。这是O(n) 操作。

      就复杂性而言,这两种操作是相等的。但是,正如其他人指出的那样,realloc 可能会避免重新定位数组,在这种情况下,将元素添加到数组中是O(1)。其他人还指出,您程序的绝大多数时间可能都花在malloc/realloc 中,这两种实现每次相加都会调用一次。

      最后,阵列可能更快的另一个原因是缓存一致性和线性副本的普遍高性能。跳转到它们之间存在明显差距的不稳定地址(较大的元素和malloc 簿记)通常不如批量复制相同数量的数据快。

      【讨论】:

      • 任何合理的链表实现都会维护一个指向链表两端的指针,所以你不需要遍历它的整个长度来添加到尾部。
      • “一个链表,用来记录头部、尾部和大小。”
      【解决方案6】:

      在这两种情况下编译器的输出会有很大不同吗?

      【讨论】:

        【解决方案7】:

        你是对的,realloc 只会增加分配块的大小,除非它被阻止这样做。在现实世界的场景中,您很可能会在随后添加到列表之间的堆上分配其他对象?在这种情况下,realloc 将不得不分配一个全新的内存块并复制列表中已有的元素。

        尝试为每十次左右的插入使用 malloc 在堆上分配另一个对象,看看它们是否仍然执行相同的操作。

        【讨论】:

        • +1,这将是一个很好的测试用例。测试是否发生重新分配也很容易,比较从 realloc 返回的新指针和旧指针。
        【解决方案8】:

        所以您正在测试扩展数组与链表的速度有多快?

        在这两种情况下,您都在调用内存分配函数。通常,内存分配函数会从操作系统中获取一块内存(可能是一个页面),然后根据应用程序的需要将其分成更小的部分。

        另一个假设是,realloc() 有时会吐出虚拟对象并在其他地方分配一大块内存,因为它无法在当前分配的页面中获得连续的块。如果您在列表展开之间没有对内存分配函数进行任何其他调用,则不会发生这种情况。也许您的操作系统对虚拟内存的使用意味着您的程序堆正在连续扩展,而不管物理页面来自何处。在这种情况下,性能将与一堆 malloc() 调用相同。

        预计性能会改变您混淆 malloc() 和 realloc() 调用的位置。

        【讨论】:

        • “另一个假设是,realloc() 会不时吐出虚拟对象并在其他地方分配一大块内存,因为它无法在当前分配的页面中获得连续的块” - 这正是我预期会导致性能下降的原因,但它没有发生。
        猜你喜欢
        • 1970-01-01
        • 2016-12-19
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-03-11
        • 1970-01-01
        • 1970-01-01
        • 2013-10-31
        相关资源
        最近更新 更多