realloc 调用会引入多少开销?
Posted
技术标签:
【中文标题】realloc 调用会引入多少开销?【英文标题】:How much overhead do realloc calls introduce? 【发布时间】:2011-07-25 04:45:09 【问题描述】:我在 for
循环的每次迭代中都使用 realloc
,该循环迭代次数超过 10000 次。
这是一个好习惯吗? realloc
被多次调用会报错吗?
【问题讨论】:
什么异常?你是说C++吗?使用 C++ 的东西。你是说C吗? C中没有例外。 请不要标记问题 C 和 C++。答案通常取决于您实际使用的语言。在 C++ 中,我会问你为什么要手动管理内存? C 函数中没有异常,但如果重新分配失败,您将面临返回空指针的风险。为什么不分配一个合理大小的缓冲区并保留它,直到您需要更大的东西?还是使用为您管理内存的标准容器? 改用容器? 【参考方案1】:我想我会在这个讨论中添加一些经验数据。
一个简单的测试程序:
#include <stdio.h>
#include <stdlib.h>
int main(void)
void *buf = NULL, *new;
size_t len;
int n = 0, cpy = 0;
for (len = 64; len < 0x100000; len += 64, n++)
new = realloc(buf, len);
if (!new)
fprintf(stderr, "out of memory\n");
return 1;
if (new != buf)
cpy++;
printf("new buffer at %#zx\n", len);
buf = new;
free(buf);
printf("%d memcpys in %d iterations\n", cpy, n);
return 0;
x86_64 上的 GLIBC 产生以下输出:
new buffer at 0x40
new buffer at 0x80
new buffer at 0x20940
new buffer at 0x21000
new buffer at 0x22000
new buffer at 0x23000
new buffer at 0x24000
new buffer at 0x25000
new buffer at 0x26000
new buffer at 0x4d000
new buffer at 0x9b000
11 memcpys in 16383 iterations
x86_64 上的 musl:
new buffer at 0x40
new buffer at 0xfc0
new buffer at 0x1000
new buffer at 0x2000
new buffer at 0x3000
new buffer at 0x4000
new buffer at 0xa000
new buffer at 0xb000
new buffer at 0xc000
new buffer at 0x21000
new buffer at 0x22000
new buffer at 0x23000
new buffer at 0x66000
new buffer at 0x67000
new buffer at 0xcf000
15 memcpys in 16383 iterations
所以看起来您通常可以依靠 libc 来处理不跨越页面边界的大小调整,而无需复制缓冲区。
在我看来,除非您能找到一种方法来使用完全避免复制的数据结构,否则请跳过应用程序中的 track-capacity-and-do-power-of-2-resized 方法,让您的libc 为您完成繁重的工作。
【讨论】:
【参考方案2】:我最近偶然发现了这个问题,虽然它很老,但我觉得信息并不完全准确。
关于一个额外的循环来预先确定需要多少字节的内存,
使用额外的循环并不总是或什至经常更好。预先确定需要多少内存涉及什么?这可能会导致额外的昂贵且不需要的 I/O。
关于一般使用 realloc,
alloc 系列函数(malloc、calloc、realloc 和 free)非常高效。底层分配系统从操作系统分配一个大块,然后根据请求将部分传递给用户。对 realloc 的连续调用几乎肯定会在当前内存位置增加额外的空间。
如果系统从一开始就更有效、更正确地为您维护堆池,您就不想自己维护堆池。
【讨论】:
【参考方案3】:在 C 中:
使用得当,realloc 没有任何问题。也就是说,很容易错误地使用它。请参阅Writing Solid Code,深入讨论所有搞砸调用 realloc 的方法以及它在调试时可能导致的其他复杂情况。
如果您发现自己一次又一次地重新分配同一个缓冲区,只是增加了一个小的增量大小,请注意,分配比您需要的更多空间通常更有效,然后跟踪实际使用的空间。如果超出分配的空间,则分配一个更大的新缓冲区,复制内容,然后释放旧缓冲区。
在 C++ 中:
您可能应该避免使用 realloc(以及 malloc 和 free)。尽可能使用标准库中的容器类(例如 std::vector)。它们经过了良好的测试和优化,可以减轻您正确管理内存(例如处理异常)的大量内务细节的负担。
C++ 没有重新分配现有缓冲区的概念。而是以新的大小分配一个新的缓冲区,复制内容,并删除旧的缓冲区。这就是 realloc 在无法满足现有位置的新大小时所做的事情,这使得 C++ 的方法看起来效率较低。但是很少有 realloc 实际上可以利用就地重新分配。并且标准 C++ 容器非常聪明地以最小化碎片的方式进行分配,并在多次更新中分摊成本,因此如果您的目标是提高性能,通常不值得努力追求 realloc。
【讨论】:
【参考方案4】:除了前面所说的,还有一些事情需要考虑:
realloc(<X-sized-buf>, X + inc)
的性能取决于两件事:
malloc(N + inc)
的速度通常会随着分配块的大小而降低到O(N)
memcpy(newbuf, oldbuf, N)
的速度,也就是O(N)
的块大小
这意味着对于 small 增量但 large 现有块,realloc()
的性能相对于现有数据块的大小是 O(N^2)
。想想冒泡排序与快速排序……
如果你从一个小块开始,它相对便宜,但如果要重新分配的块很大,它会严重惩罚你。为了减轻影响,您应该确保inc
相对于现有大小不小;按常数重新分配会导致性能问题。
此外,即使您以较大的增量增长(例如,将新大小缩放为旧大小的 150%),重新分配大缓冲区也会导致内存使用高峰;在复制现有内容期间,您使用两倍的内存量。一系列:
addr = malloc(N);
addr = realloc(addr, N + inc);
因此失败(远)早于:
addr[0] = malloc(N);
addr[1] = malloc(inc);
有些数据结构不需要realloc()
来增长;链表、跳过列表、区间树都可以追加数据,而无需复制现有数据。 C++ vector<>
以这种方式增长,它从一个初始大小的数组开始,如果超过这个值,它会继续追加,但它不会 realloc()
(即复制)。考虑实现(或使用预先存在的实现)类似的东西。
【讨论】:
说到内存峰值,我见过的realloc
最愚蠢的用途之一是调整缓冲区的大小,而不是仅仅释放它并分配一个新的内容...
Ack,在realloc(buf, size++)
魔术之后……坏主意层出不穷。
realloc
的 O(N^2) 是如何得出的?每个 O(N) 的两个单独的操作仍然被认为只是 O(N)。为了获得 O(N^2),您必须对 N
中的每个项目 n
执行另一个 O(N) 复杂性操作。
@Jason:你说得对,我错了。也就是说......如果你说它是(i + k)*O(N)
和i
的份额malloc()
和k
的份额memcpy()
,你最终仍然会得到k >> i
用于大内存块 - 成本你可能不会想承受。我对 C++ vector<>
的声明也不再正确;该行为在 C++11 之前是允许的,但 C++11 需要向量内容的连续内存,因此无法再避免在调整大小时复制。【参考方案5】:
如果您在循环中使用 realloc()-ing 相同的缓冲区,只要您有足够的内存来应对额外的内存请求,我认为没有问题 :)
通常 realloc() 会扩展/缩小您正在处理的现有分配空间,并返回相同的指针;如果它未能就地这样做,则涉及副本和免费,因此在这种情况下 realloc() 变得昂贵;你也会得到一个新的指针:)
【讨论】:
我认为“恐怖”而不是“荣誉”是一种弗洛伊德式的失误。 :-) 当然调用 realloc() 10000 次看起来像是优柔寡断的极端情况。为什么不选择一个合理的尺寸并保持它呢? 这是一个失误,好吧,因为我认为自己是一个junger :) 极端是一个艰难的词,那么穷人的快速工具对抗一个聪明但复杂的算法呢?重新,“设置合理的大小”,这就是 realloc 的确切用途,当人们无法正确计算数字时。例如,我正在考虑 getline(3) 的 impl;软件测试员也必须养家糊口,对吧?如果没有这些犹豫不决,他会在哪里?如果使用不当,realloc 可能会喂饱饥饿的人;另一方面,每个未释放的指针都会杀死一只小猫!救救小猫!【参考方案6】:您应该重新分配大小为 2 的幂。这是 stl 使用的策略,并且由于内存的管理方式而很好。 realloc 不会失败,除非您的内存不足(并将返回 NULL),但会将现有(旧)数据复制到新位置,这可能是一个性能问题。
【讨论】:
STL 实现可能有一个优势,即知道实现中的默认内存分配器是什么。我曾在系统上工作,其中 2 的幂是有效使用内存方面最差的大小,因为分配器必须添加一个小标题,然后 然后 将所需的大小四舍五入到一个偶数块。在这种情况下,2 的幂几乎可以最大化未使用的空间。 二的幂没有什么神奇的。你应该只是realloc
以指数增加的大小来避免O(n^2)
循环性能,但基数可以是任何大于 1 的值,不一定是 2。很多人喜欢 1.5(每次用完缓冲区时增加 50%空间)。
@Steve:是的,但如果是这种情况,这是可以处理的特殊情况。 @R。这并不神奇,但分配大小为 2 的幂是最佳的:),原因是页面大小可以是 4k 或 2Mb。
@cprogrammer 您可能会匹配您分配的块的页面大小,但也有开销。子分配的因素也是如此,您的内存请求是由我的子分配器而不是主系统分配器处理的。所以,这个论点当然没有显示出 2 的幂的最优性。
@cprogrammer 你没有设置分配器。您的 C 或 C++ 库附带一个。它将从系统中获取内存,然后从中进行子分配。因此,虽然您可能认为使用 2 的幂和等于页面大小倍数的值调用 malloc(或任何分配函数)是聪明的,但这一切都被库所吞噬,该库分配更大的块并从内部进行子分配。毫无疑问,最好的策略是使用标准容器。【参考方案7】:
如果你这样做,你会冒着使你的记忆碎片化的风险。这会导致性能下降,并且对于 32 位系统,由于缺乏大的连续内存块的可用性,可能会导致内存短缺。
我猜你每次都会将数组的长度增加 1。如果是这样,那么您最好跟踪容量和长度,并且仅在需要超过当前容量的长度时才增加容量。当您增加容量时,增加的数量不仅仅是 1。
当然,标准容器会为你做这种事情,所以如果你可以使用它们,最好这样做。
【讨论】:
【参考方案8】:除非您的内存用完(任何其他分配器也会发生这种情况),否则它不会失败 - 但如果您设法预先估计所需的存储空间,您的代码通常会运行得更快。
通常最好只运行一个额外的循环来确定存储要求。
我不会说realloc
是不行的,但这也不是好的做法。
【讨论】:
如果您可以运行一个额外的循环来确定存储,那么这样做是很好的。但在许多情况下,这实际上是不可能的,因为您需要在每个项目到达时一劳永逸地处理它。 即使没有额外的循环,您也可以通过经验法则启发式减少重新分配的数量,例如增加分配的内存量作为总大小的一个因素,而不是一次只一个对象(例如,您可能从 100 个对象的空间开始,当空间已满时再增加 50%(使总数达到 150),然后再增加 50%(至 225),再增加(至 338)等等......跨度> 是的,如果您需要使用realloc
(即在 David 所描述的情况下,省略了明显的 C++ 替代方案),请务必小心使用它。为 每个 单循环迭代重新分配是一个坏主意。但我认为寻找数组的最佳增长因子是一个不同的话题,在 SO 上已经有很多争论。
"[R]un out of memory" 可能过于简化了。当内存碎片化时,即使有足够的空间,分配也会失败,但它根本不连续。由于该问题强烈暗示了许多增量重新分配,因此碎片似乎是一个真正的问题。
一个额外的循环肯定会引入比多次调用 realloc 更昂贵的开销。 alloc 系列函数非常高效,并且会比用户维护自己的堆池做得更好、更高效。以上是关于realloc 调用会引入多少开销?的主要内容,如果未能解决你的问题,请参考以下文章
我可以假设调用较小大小的 realloc 会释放其余部分吗? [复制]
是真的吗,现代操作系统在调用 realloc 时可能会跳过复制