Malloc与自定义分配器:Malloc有很多开销。为什么?

12

我有一个图像压缩应用程序,它现在具有两个不同版本的内存分配系统。在原始版本中,malloc被广泛使用,在第二个版本中,我实现了一个简单的池分配器,它只分配一块连续的内存并将该内存的部分返回给myalloc()调用。

当使用malloc时,我们注意到有巨大的内存开销:在其内存使用高峰期,malloc()代码为1920x1080x16bpp图像需要约170兆字节的内存,而池分配器仅分配了48兆字节,其中47兆字节由程序使用。

就内存分配模式而言,该程序使用测试图像大量分配8字节(最多)、32字节(较多)和1080字节块(一些)。除此之外,代码中没有动态内存分配。

测试系统的操作系统是Windows 7(64位)。

我们如何测试内存使用情况?

使用自定义分配器,我们可以看到使用了多少内存,因为所有malloc调用都被延迟到分配器。对于malloc(),在Debug模式下,我们只需逐步执行代码并在任务管理器中观察内存使用情况即可。在发布模式下,我们也是这样做的,但不太精细,因为编译器会将许多内容进行优化,所以我们无法逐个代码段地执行(发布和调试模式之间的内存差异约为20MB,我认为这是由于优化和发布模式中缺乏调试信息造成的)。

malloc单独可能是导致如此巨大开销的原因吗?如果是这样,malloc内部到底发生了什么导致了这种开销?


通常只有当你编写的内容知道了如何使用的具体细节并且会在性能上增加很多好处时,你才会编写一些通用东西的自定义版本。然而,我不认为使用 malloc 会增加内存开销。你确定你正确地测量了内存使用情况吗?你确定在使用 malloc 时正确释放了内存吗? - CashCow
malloc代码中的所有内容都被释放了(我使用内存分析器进行了测试),但是只有在应用程序非常结束之前才会发生,因此测量发生在任何free()函数被调用之前(在两个版本中都是如此)。自定义分配器加速了整个过程,并为我们节省了大约15毫秒的图像处理时间(因为它只是一个大的分配而不是许多小的分配)。 - TravisG
malloc在你释放内存后可能会“保留”一些内存以供立即使用。如果你的实现正在使用std::vector,你可以提前“reserve”,尽管当你要分配如此大量的内存时,最好不要选择需要连续缓冲区的模型。 - CashCow
1
你能描述一下你们是如何测量内存使用情况的吗? - UmNyobe
通过自定义分配器,我们可以看到使用了多少内存,因为所有的malloc调用都被延迟到分配器中。对于malloc(),在Debug模式下,我们只需逐步执行代码并在任务管理器中观察内存使用情况。在Release模式下,我们也是这样做的,但粒度较低,因为编译器会优化很多东西,所以我们无法逐个代码块地执行(发布版和调试版之间的内存差异约为20MB,我认为这归因于优化和发布模式中缺乏调试信息)。 - TravisG
显示剩余3条评论
3个回答

9
在Windows 7上,您将始终获得低碎片堆分配器,而不必显式调用HeapSetInformation()来请求它。该分配器通过牺牲虚拟内存空间来减少碎片化。实际上,您的程序并没有使用170 MB,您只是看到一堆自由块围绕着等待类似大小的分配。
这种算法非常容易被一个不会做任何减少碎片化的定制分配器所击败。虽然这可能适合您,但直到您让程序运行超过单个调试会话才能看到其副作用。如果预期使用模式,则确保其稳定数天或数周。
最好的方法就是不要担心它,170 MB相当小。请记住,这是虚拟内存,不需要任何成本。

2
在Windows 7上,您将始终获得低碎片堆分配器,除非您在调试器下运行。根据所描述的方法(通过代码步进),似乎在测量内存消耗时使用了调试器,因此未使用LFH。请参见我的答案。 - Suma
1
那篇文章引用了古老的历史,内存管理器在Vista中得到了显着修订。但是,堆调试功能使用的块填充确实会影响他看到的VM使用量。 - Hans Passant
虚拟内存是有代价的,页面失效会对性能造成痛苦。 - Fingolfin
不,你不能从访问一个空闲块中得到页面错误。那将是一个错误。 - Hans Passant
@Hans 可能我理解错了,你能否进一步解释一下? - Fingolfin
虚拟内存至少按4k的页面分配;除非我们谈论的是没有页表的分段内存管理,这可能不被任何现代操作系统支持。 - Aki Suihkonen

6

首先,malloc将指针对齐到16字节边界。此外,在返回值之前的地址中至少存储一个指针(或分配长度)。然后,它们可能会添加一个魔术值或释放计数器来指示链接列表没有被破坏或内存块没有被释放两次(free ASSERTS for double frees)。

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

int main(int ac, char**av)
{
  int *foo = malloc(4);
  int *bar = malloc(4);
  printf("%d\n", (int)bar - (int)foo);
}

返回值:32


32位意味着占用16个字节吗? - TravisG
2
@UmNyobe:显然是我的错误。无论如何,8字节的malloc需要32字节,而32字节的malloc则至少需要48字节。 - Aki Suihkonen
谢谢。对齐可能解释了很多内存丢失问题。Aki,你有这方面信息的任何来源吗? - TravisG
请访问以下链接:http://www.gnu.org/software/libc/manual/html_node/Aligned-Memory-Blocks.html。然后,这取决于malloc的实现方式:http://bardofschool.blogspot.com/2009/09/glibc-malloc-implementation.html 讨论了由于malloc必须避免碎片并且必须快速搜索适当大小的空闲块,因此他们可能会为不同大小的分配单元实现几种机制。 - Aki Suihkonen
1
一个 malloc 实现并不一定需要添加头部。像 jemalloc 这样的现代分配器实现即使对于小型分配(~2%)也具有非常低的元数据开销。通过使用对齐块,可以通过对齐检查(jemalloc 中的 4M 块)将小型(< chunk)分配与大型(>= chunk)分配区分开来。小型分配的元数据可以通过全局数据结构的偏移量找到,而大型分配则可以使用全局数据结构(这就是 TCMalloc 所做的)。 - strcat
显示剩余3条评论

4

注意:当您在Visual Studio或带有任何调试器的情况下运行程序时,默认情况下malloc行为会发生很大变化,不使用低碎片堆,而内存开销可能不代表实际使用情况(另请参见https://stackoverflow.com/a/3768820/16673)。您需要使用环境变量_NO_DEBUG_HEAP=1来避免受到影响,或者在不在调试器下运行时测量内存使用情况。


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