【问题标题】:Why is realloc eating tons of memory?为什么 realloc 会消耗大量内存?
【发布时间】:2011-05-10 13:15:42
【问题描述】:

由于源代码,这个问题有点长,我尽量简化了。请多多包涵,感谢您的阅读。

我有一个带有可能运行数百万次循环的应用程序。与其在该循环中调用数千到数百万个malloc/free,不如先调用一个malloc,然后再调用数千到数百万个realloc

但是当我使用realloc 时,我遇到了一个问题,即我的应用程序消耗了几 GB 的内存并自行终止。如果我使用malloc,我的内存使用情况很好。

如果我使用valgrind 的memtest 在较小的测试数据集上运行,它会报告mallocrealloc 没有内存泄漏。

我已验证我将每个malloc-ed(然后是realloc-ed)对象与相应的free 匹配。

所以,理论上,我没有泄漏内存,只是使用 realloc 似乎消耗了我所有的可用 RAM,我想知道为什么以及我可以做些什么来解决这个问题。

我最初拥有的是这样的东西,它使用malloc并且可以正常工作:

Malloc 代码

void A () {
    do {
        B();
    } while (someConditionThatIsTrueForMillionInstances);
}

void B () {
    char *firstString = NULL;
    char *secondString = NULL;
    char *someOtherString;

    /* populate someOtherString with data from stream, for example */

    C((const char *)someOtherString, &firstString, &secondString);

    fprintf(stderr, "first: [%s] | second: [%s]\n", firstString, secondString);

    if (firstString)
        free(firstString);
    if (secondString)
        free(secondString);
}

void C (const char *someOtherString, char **firstString, char **secondString) {
    char firstBuffer[BUFLENGTH];
    char secondBuffer[BUFLENGTH];

    /* populate buffers with some data from tokenizing someOtherString in a special way */

    *firstString = malloc(strlen(firstBuffer)+1);
    strncpy(*firstString, firstBuffer, strlen(firstBuffer)+1);

    *secondString = malloc(strlen(secondBuffer)+1);
    strncpy(*secondString, secondBuffer, strlen(secondBuffer)+1);
}

这很好用。但我想要更快的东西。

现在我测试一个realloc 排列,其中malloc-s 只有一次:

重新定位代码

void A () {
    char *firstString = NULL;
    char *secondString = NULL;

    do {
        B(&firstString, &secondString);
    } while (someConditionThatIsTrueForMillionInstances);

    if (firstString)
        free(firstString);
    if (secondString)
        free(secondString);
}

void B (char **firstString, char **secondString) {
    char *someOtherString;

    /* populate someOtherString with data from stream, for example */

    C((const char *)someOtherString, &(*firstString), &(*secondString));

    fprintf(stderr, "first: [%s] | second: [%s]\n", *firstString, *secondString);
}

void C (const char *someOtherString, char **firstString, char **secondString) {
    char firstBuffer[BUFLENGTH];
    char secondBuffer[BUFLENGTH];

    /* populate buffers with some data from tokenizing someOtherString in a special way */

    /* realloc should act as malloc on first pass through */

    *firstString = realloc(*firstString, strlen(firstBuffer)+1);
    strncpy(*firstString, firstBuffer, strlen(firstBuffer)+1);

    *secondString = realloc(*secondString, strlen(secondBuffer)+1);
    strncpy(*secondString, secondBuffer, strlen(secondBuffer)+1);
}

如果我在使用导致百万循环条件的大型数据集运行此基于realloc 的测试时在命令行上查看free -m 的输出,我的内存将从4 GB 下降到0并且应用崩溃了。

使用realloc 导致这种情况我错过了什么?对不起,如果这是一个愚蠢的问题,并提前感谢您的建议。

【问题讨论】:

  • 你也可以显示 strace 的输出吗?知道系统调用 realloc 与 malloc/free 映射到什么上会很有启发性。
  • 您可能想研究一个自定义分配函数,该函数基本上通过 malloc() 获取一个大块并将其作为自定义堆空间进行管理。这是一种老派,但如果你知道自己在做什么,它会很好地工作。

标签: c malloc realloc


【解决方案1】:

realloc 必须将内容从旧缓冲区复制到新缓冲区,如果无法完成调整大小操作。如果您不需要保留原始内存,malloc/free 对可能比 realloc 更好。

这就是为什么realloc 可能暂时需要比malloc/free 对更多的内存。您还通过不断交错reallocs 来鼓励碎片化。即,您基本上是在做:

malloc(A);
malloc(B);

while (...)
{
    malloc(A_temp);
    free(A);
    A= A_temp;
    malloc(B_temp);
    free(B);
    B= B_temp;
}

而原来的代码是这样的:

while (...)
{
    malloc(A);
    malloc(B);
    free(A);
    free(B);
}

在每个第二个循环结束时,您已经清理了所有使用的内存;与在不完全释放所有内存分配的情况下交错内存分配相比,这更有可能将全局内存堆返回到干净状态。

【讨论】:

  • 有没有办法定期刷新碎片堆,比如每 x 次迭代?
  • @Alex,这取决于实际的分配器。请注意,我的碎片化想法并不准确。它实际上取决于底层实现。如果您使用的是 Windows,您可以尝试在全局堆上使用 HeapCompact,但这并不能真正保证做任何事情。
  • 嗯,这整个碎片化的事情似乎与我所听到和读到的关于如何建议使用 realloc 的内容相矛盾,其中有很多昂贵的 malloc/free 调用。也许我应该查看堆栈上的固定大小缓冲区。 :(
  • @Alex:跟踪firstStringsecondString 的当前分配大小。仅在它们需要增长时才打扰realloc 它们,并量化它们的大小(例如,使它们成为您需要的大小的下一个最大的 2 次方)。
  • @Alex,再一次,碎片化的想法并不那么精确;这真的取决于分配器。使用非常简单的基于堆栈的分配器(即,它只回收连续的内存并以分配的区域结束),您可以使用基于 realloc 的策略获得病态的不良分配行为。在调整到一般模式的不同分配器上,您可能不会。这实际上取决于分配器。但 realloc 通常在最坏的情况下需要旧大小加上新大小才可用。
【解决方案2】:

您期望&(*firstString)firstString 相同,但实际上它是将参数的地址传递给您的函数,而不是通过A 中的指针地址。因此,每次您调用时,您都会创建一个 NULL 的副本,重新分配新内存,丢失指向新内存的指针,然后重复。您可以通过查看A 末尾的原始指针仍然为空来轻松验证这一点。

编辑:嗯,这是一个很棒的理论,但我似乎对我可以测试的编译器有误。

【讨论】:

  • 我不确定我是否理解。 B中的fprintf语句对于两个字符串的值没有显示NULL,即分词和strncpy成功。
  • 对不起,我说B我的意思是A(在第2版中);当我获取函数名称时,我在问题中向后滚动得太远了。
  • 如果我使用小型数据集,realloc 应用程序(版本 2)可以工作,即我看不到 NULLfirstStringsecondString 值,因为复制事件成功.
  • &*first_string 与 first_string 相同。如果不是这样,我会感到无限惊讶。
【解决方案3】:

当您不想保留内存块的现有内容时使用realloc 是一个非常非常糟糕的主意。如果不出意外,您将浪费大量时间来复制您将要覆盖的数据。实际上,按照您使用它的方式,调整大小的块将不适合旧空间,因此它们位于堆上越来越高的地址,导致堆变得荒谬。

内存管理并不容易。糟糕的分配策略会导致碎片化、糟糕的性能等。你能做的最好的事情就是避免引入比你绝对需要的更多的约束(比如在不需要时使用realloc),尽可能多地释放内存完成后,将大块关联数据一起分配到单个分配中,而不是小块。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-05-12
    • 1970-01-01
    • 2019-08-05
    • 2017-06-17
    • 2019-09-30
    • 2018-10-10
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多