【问题标题】:C - Is realloc'ing arrays to arrays twice the size efficient in dynamic data structures?C - 在动态数据结构中,将数组重新分配到两倍大小的数组是否有效?
【发布时间】:2015-06-04 16:17:26
【问题描述】:

最近我一直在用 C 编写大量代码,并且我注意到,在我的程序中花费最多时间的事情是调整动态数据结构的大小。假设我们有一个包含字符的数组,我想在这个数组的末尾附加一些字符。我是这样做的:

  1. 检查是否分配了足够的内存。
  2. 如果不是,则将数组重新分配到大小为两倍的数组(使用 realloc)
  3. 如果现在有足够的内存,追加字符,否则转到第 2 点。

我只是想知道这是否足够有效。例如,我可以考虑类似树结构的东西,我会将数组保存在树的节点中,这样我就不会在添加之前将旧元素复制到新数组中,而是将新的 malloc'ed 元素添加到下一个树的节点并在那里附加字符。这样我就可以避免不必要的复制...

所以这只是以不同方式调整大小的一种方法。我应该寻找其他东西还是将旧元素复制到两倍大小的新数组的解决方案足够有效?

【问题讨论】:

  • 寻求基于意见的答案的问题在这里是不合适的,所以我投票结束了这个问题。不过,您需要考虑一件重要的事情——您认为您可能需要将内存翻倍(以及您有多少内存可以继续分配)?您是否会达到仅添加一个条目而加倍的系统资源成本太高的地步?如果您可以合理地使用链表/树等,而不是单个大缓冲区,那么您可能应该这样做。
  • 一般来说,复杂的树永远不值得花时间去实现它。 1.5 的 realloc 就好了。
  • @user3125367 是的,应该真正使用已经提供它们的语言。 C 不是其中之一。
  • @PeterSchneider 您的评论似乎有点讽刺(没关系)。实际上,复杂的树在任何语言中都很难,除非实现隐藏在某个接口下。然后您会体验到该接口的开销,因为将索引和迭代器映射到“平凡的用户空间”变得不平凡。可以从 sourceforge 等获取无数个 adt 库中的任何一个并使用它,因此 C 确实 提供了工具。不过,复杂性的价值仍然是一个大问题。
  • @DrunkCoder 有趣的见解,谢谢。

标签: c arrays data-structures


【解决方案1】:

与所有“最有效”的问题一样,唯一知道的方法是衡量现实条件下的实际表现。

一般来说,将项目放在连续的内存位置可以大大提高性能,因为对于任何一个接一个地处理项目的算法来说,这几乎总是对缓存最友好的布局。总体而言,连续数组使用的空间也更少,这仅仅是因为您不需要额外的空间来存储额外的数据(如树节点)。

虽然在需要增长时将大小加倍是一种常见的方法,但它可能往往会占用地址空间。一些 STL 实现将向量之类的结构增长 1.5 倍而不是 2 倍,以避免某些病态情况。在短期内,这会导致更多的分配,但会导致更少的内存碎片。如果您使用 realloc,这可能适用也可能不适用。这取决于 realloc 多久成功完成一次。

【讨论】:

  • 前段时间我也读到过 1.5x 的碎片较少,但找不到链接。因此,它似乎不像 op-commenter 所说的那样基于意见。 OP 注意:如果您正在积极调整大小并计划在删除时缩小(即不仅增长),请避免使用相同的因素。例如。增长 1.5 倍,但仅缩小 0.5 倍。这将防止近边界添加/删除情况下的 realloc 抖动。
【解决方案2】:

通常任何具有对数时间复杂度的事物(例如您的重新分配算法)都被认为“足够好”。通常,您只会使用块列表保存复制操作,但您仍然必须访问新块的免费存储,这总是相对昂贵。

您最好的选择可能是对预分配执行合理的启发式方法,以便在大多数情况下完全避免重新分配。今天的记忆很丰富,至少如果其他人表现出一些纪律的话;-)。

【讨论】:

    【解决方案3】:

    接受回答后

    效率取决于许多问题,包括分析将为给定的淤积提供一个想法。

    当然,按比例因子(例如 2.0、1.5 或邻域中的某个值)增长会提供很好的对数增长。

    2 其他地方未涉及的问题:收缩和乐观分配。

    收缩:结构不仅会增长,而且可能会收缩。像增长一样,减少应该是对数的。然而,如果增长/减少的阈值相似,减少可能会发生颠簸。建议增长和收缩阈值对数均匀分布。示例:4x 步,

    加大码 1、4、16、64、256 ...
    收缩 2, 8, 32, 128 ...

    乐观分配:内存容量大的处理器使用的虚拟地址空间通常远大于物理地址空间。在分配内存时,比如 1,000,000,在使用之前不会分配真正的物理内存 - (例如,设置为非零)。即使这样,它也仅用于某些页面大小的块,例如 4k 或 64k 字节。见Why is malloc not "using up" the memory on my computer?

    【讨论】:

      【解决方案4】:

      将分配增加两倍的基本想法是可靠的,因为它使平均重新分配工作与最大数组大小成线性关系(每个对象平均移动一次,因为一半对象从未移动)。

      我在您的实现中看到的唯一可优化点是,当数组的大小必须跳转时,您会重新分配多次:如果您有一个 64 字节的数组并且需要 4096 字节,您的 realloc() 调用首先复制 64 字节,然后是 128 个字节(其中 64 个未初始化),然后是 256 个字节,依此类推,总共复制 4032 个字节,途中进行了 6 次分配。然而,所需要的只是获取 4096 个字节的内存并复制这 64 个字节。以一种在增加分配时最多调用一次realloc() 的方式编写代码。

      另一个影响性能的因素是起始尺寸太小。 malloc() 调用需要超过 250 个 CPU 周期,并且分配具有至少两个指针大小值的开销。您希望确保这些成本相对于您的内存使用量而言很小。因此,通常只对一个元素使用初始分配是没有意义的。您要确保初始分配至少在 64 字节范围内(25% 的空间开销)。

      【讨论】:

        【解决方案5】:

        重新分配数组绝对不是最有效的。 Realloc 确保连续的内存块。由于这可能是不可能的,realloc 最终可能会分配新的内存块,将所有数据复制到其中,然后释放原始内存块。这可能非常昂贵,因为就数据大小而言,它是一种线性操作,具有相当大的恒定开销。

        您应该真正评估是否需要连续的内存块(您描述的树结构是非连续数据结构的示例)。如果你这样做了,你应该尝试想出一种技术来估计你需要多少内存,并尽量减少重新分配。

        在我看来,您可以将数据存储在指向其他字符数组的指针数组中。由于顶级数组仅存储指针,因此您可以为所有实际用途预先分配足够大的空间。不过,我对您的问题知之甚少,无法推荐合适的解决方案。

        【讨论】:

          猜你喜欢
          • 2018-11-26
          • 2013-01-22
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2019-07-19
          • 2017-07-04
          • 2014-08-22
          • 2014-12-09
          相关资源
          最近更新 更多