realloc但只有前几个字节有意义

8
假设我已经使用ptr = malloc(old_size);来分配一个有old_size字节的内存块。只有前header_size字节是有意义的。现在我将增加尺寸到new_size

new_size大于old_size,而old_size大于header_size

之前:

/- - - - - - - old_size - - - - - - - \
+===============+---------------------+
 \-header_size-/

之后:

/- - - - - - - - - - - - - - - new_size - - - - - - - - - - - - - - - - - - -\
+===============+------------------------------------------------------------+
\- header_size-/

我不在乎在ptr + header_size之后存储了什么,因为我会将一些数据读取到那里。

方法1:直接转到new_size

ptr = realloc(ptr, new_size);

方法二:缩小到header_size并增大到new_size

ptr = realloc(ptr, header_size);
ptr = realloc(ptr, new_size);

方法三:分配一个新的内存块并复制前header_size个字节

void *newptr = malloc(new_size);
memcpy(newptr, ptr, header_size);
free(ptr);
ptr = newptr;

哪个更快?


@nos 我会在重新分配后写入一些超出 header_size 的数据,所以我不关心里面有什么。 - lqs
3个回答

3

无论是 malloc(用于整个块)还是 realloc(当增加大小时,用于旧块大小以外的空间),都不能保证您收到的内存包含什么内容。因此,如果您希望将这些多余的字节设置为零(例如),则需要使用类似以下代码的方式自行完成:

memset(pointer_to_memory_block, 0, size_of_allocated_memory_block);
// ptr contains current block.
void *saveptr = ptr;
ptr = realloc (ptr, new_size);
if (ptr == NULL) {
    // do something intelligent like recover saveptr and exit.
}
memset (ptr + header_size, 0, new_size - header_size);

然而,由于您已经声明您不关心标题以外的内容,最快的选择几乎肯定是单个realloc,因为它很可能在底层进行了优化。
虽然收缩和扩展需要两次调用或者调用malloc-new/memcpy/free-old很可能不够高效,但是与所有优化一样,您应该测量,不要猜测! 请注意,realloc不一定需要复制您的内存。如果可以原地扩展,则智能堆管理器将只增加块的大小而不复制任何内容,例如:
+-----------+   ^        +-----------+ <- At same address,
| Old block |   | Need   | New block |      no copying
|           |   | this   |           |      involved.
+-----------+   | much   |           |
| Free      |   | now.   |           |
|           |   v        +-----------+
|           |            | Free      |
|           |            |           |
+-----------+            +-----------+

1
当然啊,realloc()保证新的内存将包含尽可能多的旧块。 - unwind
1
@unwind:我是指超出旧边界,而不是旧块中的记忆。我会澄清的。 - paxdiablo
@lqs,由于您在评论中表示您不关心非标题包含的内容,因此我简化了我的答案以适应。 - paxdiablo

2
几乎肯定取决于 old_size、new_size 和 header_size 的值,还取决于具体实现。您需要选择一些值并进行测量。
1)当 header_size == old_size-1 && old_size == new_size-1 时,这种情况下最好使用 1,因为它可以让单个 realloc 基本上成为无操作。在这种情况下,2 只会稍微慢一点(2 个几乎无操作比 1 稍微慢一点)。
2)当 header_size 远小于 old_size 且 new_size 处于一个合理的范围内时,realloc 很可能会重新分配空间,但也很可能不会。此时,2 可能是最好的选择。您无法预测哪个更快(1 还是 3),但两者都比 2 稍微慢一点。
3)当 header_size == 1 && old_size == 1024*1024 && new_size == 2048*1024 时,这种情况下最好使用 3,因为 realloc 需要移动分配,但您避免了复制您不关心的 1MB 数据。在这种情况下,2 只会稍微慢一点。
在分析 2 时,我假设向下 realloc 几乎是免费的,并返回相同的指针。这并不保证。有两件事可能会使您受挫:
1. 向下 realloc 复制到新的分配。 2. 向下 realloc 拆分缓冲区以创建一个新的自由内存块,但是当您再次 realloc 回来时,分配器不会将该新的自由块直接合并到您的缓冲区中,以便无需复制即可返回。
其中任何一种情况都可能使 2 比 1 更昂贵。因此,使用 2 还是取决于具体实现,它是在(1)(有时避免复制任何内容的优点)和(3)(有时避免复制太多内容的优点)之间权衡的一种好方法。
顺便说一句,这种关于性能的空想比试图预测我们在真正关心性能并进行测试时会得出什么样的结果更有效。
此外,我怀疑对于大型分配,实现可能能够执行重新定位的 realloc 而不复制任何内容,通过将内存重新映射到新地址。如果是这样,它们都会很快。不过,我还没有调查实现是否真的这样做。

1

这可能取决于大小和是否需要复制。

方法1将复制旧块中包含的所有内容 - 但如果您不经常这样做,您不会注意到。

方法2只会复制您需要保留的内容,因为您事先丢弃了其他所有内容。

方法3将无条件复制,而其他方法仅在无法在原地调整内存块大小时才进行复制。

个人而言,如果您经常这样做,我更喜欢方法2,或者如果您很少这样做,则选择方法1。分别测试哪种方法更快。


方法2是唯一一个完全避免复制(如果块后有可用空间)并且在其他情况下仅复制最小内存的方法。方法1在最佳情况下效率最高,但在最坏情况下可能也是效率最低的。这确实取决于我们谈论多少数据是否值得额外的函数开销。 - ams
@ams 有没有实际上考虑在realloc之前释放块后面的空闲块的实现?对我来说似乎不太可能。 - pmr
1
@pmr:即使他们不尝试合并块,在new_size - old_size很小的情况下,由于old_size已经被舍入,可能会在块的末尾有可用的空闲空间来满足新的大小。 - Steve Jessop
1
@pmr 如果有空闲空间,为什么不将分配扩展到其后面呢?当然,必须有人以这种方式实现它,但是有很多聪明的人在那里!但实际上,您甚至不能假设减小分配的大小不会导致复制。 - ams
1
@pmr 至少一些 malloc 的实现在分配大于一页(通常为4KiB)的内存时使用 mmap,因此扩展分配大小几乎是无操作的。这取决于地址空间有多拥挤,但在64位机器上可能不是问题。 - ams

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接