【问题标题】:Is doubling the capacity of a dynamic array necessary?是否需要将动态数组的容量翻倍?
【发布时间】:2013-12-25 05:29:26
【问题描述】:

在 C 中制作自动扩展数组(如 C++ 的 std::vector)时,通常(或至少常见的建议)每次填充数组时将数组的大小加倍以限制对 @ 的调用量987654321@为了尽量避免复制整个数组。

例如。我们首先为 8 个元素分配空间,插入 8 个元素,然后为 16 个元素分配空间,再插入 8 个元素,再分配 32 个元素,等等。

realloc如果可以扩展现有的内存分配,则不必实际复制数据。例如,以下代码在我的系统上只执行了 1 次复制(初始 NULL 分配,因此它不是真正的复制),即使它调用了 realloc 10000 次:

#include <stdlib.h>
#include <stdio.h>

int main()
{
    int i;
    int copies = 0;
    void *data = NULL;
    void *ndata;

    for (i = 0; i < 10000; i++)
    {
        ndata = realloc(data, i * sizeof(int));
        if (data != ndata)
            copies++;
        data = ndata;
    }
    printf("%d\n", copies); 
}

我意识到这个例子是非常临床的 - 一个真实世界的应用程序可能会有更多的内存碎片并且会做更多的副本,但即使我在 realloc 循环之前进行了一堆随机分配,它只会稍微更糟用 2-4 份代替。

那么,“倍增法”真的有必要吗?每次将元素添加到动态数组时,只调用realloc 不是更好吗?

【问题讨论】:

  • 您的 O(N) 算法从根本上比双倍分配提供的 O(logN) 算法差。
  • 你的算法是一个稳定增长的内存需求,没有干预释放,所以它当然不需要复制。尝试在循环中添加随机大小和随机穿插的 mallocfree 调用。
  • @Mike,你总是必须在这里做“最坏情况”的考虑:考虑每一步都需要复制的情况。在这种情况下,您使用这种方法肯定会生活得更好。不用每次都加倍,* 1.5也可以,这样会导致增长速度变慢,但每一步都比+ 1好。即使+ 1000在需要时使用它也会比+ 1更好,但扩展性不是很好。但是,如果您有一定的最大限制,那也可以。

标签: c


【解决方案1】:

你必须从你的代码中退后一分钟,抽象地做一些事情。增长动态容器的成本是多少?程序员和研究人员不会考虑“这需要 2 毫秒”,而是考虑渐近复杂度:鉴于我已经拥有 n 元素,增长一个元素的成本是多少?随着n 的增加,这种情况有何变化?

如果您只增长了一个常数(或有界)数量,那么您将不得不定期移动所有数据,因此增长的成本将取决于容器的大小并随容器的大小而增长。相比之下,当你以几何方式增长容器时,即将其大小乘以一个固定因子,每次它都满了,那么插入的预期成本实际上是与元素个数无关,即常量

当然不是总是不变,而是摊销常数,这意味着如果你不断插入元素,那么每个元素的平均成本是不变的。您必须时不时地成长和移动,但随着您插入越来越多的元素,这些事件会变得越来越少。

我曾经问过whether it makes sense for C++ allocators to be able to grow,就像realloc 那样。我得到的答案表明,当你渐近思考时,realloc 的不动增长行为实际上有点像红鲱鱼。最终你将无法再成长,你将不得不搬家,因此为了研究渐近成本,realloc 有时是否可以成为空操作实际上无关紧要。 (此外,不动的增长似乎让现代的、基于竞技场的分配器感到不安,他们希望所有的分配都具有相似的大小。)

【讨论】:

    【解决方案2】:

    与几乎所有其他类型的操作相比,malloccalloc,尤其是realloc,内存非常昂贵。我亲自对 10,000,000 个重新分配进行了基准测试,这需要花费大量时间。

    即使我同时进行了其他操作(在两个基准测试中),我发现我可以通过使用 max_size *= 2 而不是 max_size += 1 来缩短运行时间。

    【讨论】:

    • 请澄清:您通过什么方式缩短了运行时间?
    • max_size *= 2 而不是 max_size += 1
    • 好答案。在性能驱动的代码中,尤其是在服务器代码中,减少内存分配的频率是一个巨大的性能提升。
    【解决方案3】:

    问:'需要将动态数组的容量加倍"
    答:不能。一个人只能增长到需要的程度。但是,您可能会多次复制数据。这是内存和处理器时间之间的经典权衡。一个好的增长算法会考虑到程序数据需求的已知信息,并且不要过度考虑这些需求。 2 倍的指数增长是一个令人愉快的折衷方案。

    但是现在你声称“下面的代码只做 1 个副本”。

    使用高级内存分配器的复制量可能不是 OP 所想的。获得相同的地址并不意味着底层内存映射没有执行重要的工作。各种活动都在幕后进行。

    对于在代码生命周期内大幅增长和收缩的内存分配,我喜欢将增长和收缩阈值以几何方式分开放置。

    const size_t Grow[]   = {1, 4, 16, 64, 256, 1024, 4096, ... };
    const size_t Shrink[] = {0, 2,  8, 32, 128,  512, 2048, ... };
    

    通过在变大时使用增长阈值并在收缩时使用收缩阈值,可以避免在边界附近颠簸。有时会使用 1.5 的系数。

    【讨论】:

    • 您说得很好,即使分配器返回相同的地址,它也可能会做很多工作。我没有考虑到这一点。
    猜你喜欢
    • 1970-01-01
    • 2021-11-25
    • 1970-01-01
    • 2012-10-19
    • 1970-01-01
    • 2021-06-15
    • 1970-01-01
    • 2021-12-10
    • 2019-06-28
    相关资源
    最近更新 更多