每个对象内存分配有多少开销?

时间:2017-10-27 16:25:35

标签: c++ c memory allocation

比方说,如果我调用malloc(sizeof(int)),请求4个字节,系统(或std库?)将添加多少额外资源来支持内存管理基础架构?我相信应该有一些。否则,当我调用free(ptr)时,系统如何知道要处理多少字节。

更新1:这可能听起来像一个“太广泛的问题”,显然,特定于C / C ++库,但我感兴趣的是支持单个分配所需的最小额外内存。甚至不是特定的系统或实现。例如,对于二叉树,必须有2个指针 - 左右儿童,你无法挤压它。

更新2: 我决定在Windows 64上自己检查一下。

#include <stdio.h>
#include <conio.h>
#include <windows.h>
#include <psapi.h>
void main(int argc, char *argv[])
{
    int m = (argc > 1) ? atoi(argv[1]) : 1;
    int n = (argc > 2) ? atoi(argv[2]) : 0;
    for (int i = 0; i < n; i++)
        malloc(m);
    size_t peakKb(0);
    PROCESS_MEMORY_COUNTERS pmc;
    if ( GetProcessMemoryInfo(GetCurrentProcess(), &pmc, sizeof(pmc)) )
            peakKb = pmc.PeakWorkingSetSize >> 10;
    printf("requested : %d kb, total: %d kb\n", (m*n) >> 10, peakKb);
    _getch();
}

要求:0 kb,总计:2080 kb

1个字节: 要求:976 kb,总计:17788 kb 额外:17788 - 2080 - 976 = 14732(+ 1410%)

2个字节: 要求:1953 kb,总计:17784 kb 额外:17784 - 2080 - 1953 =(+ 605%以上)

4个字节: 要求:3906 kb,总计:17796 kb 额外:17796 - 2080 - 3906 = 10810(+ 177%)

8个字节: 要求:7812 kb,总计:17784 kb 额外:17784 - 2080 - 7812 =(0%)

更新3:这是我对以下问题的回答:除了速度慢之外,默认C ++分配器的通用性使得它对于小型对象来说效率非常低。默认分配器管理内存池,这种管理通常需要一些额外的内存。通常,对于使用new分配的每个块,簿记存储器相当于一些额外的字节(4到32)。如果分配1024字节块,则每块空间开销无关紧要(0.4%至3%)。如果你分配8字节的对象,那么每个对象的开销就会变成50%到400%,这个数字足以让你担心,如果你分配了很多这样的小对象。

4 个答案:

答案 0 :(得分:2)

对于已分配的对象,理论上不需要其他元数据。例如,malloc的符合实现可以将所有分配请求四舍五入到固定的最大对象大小。因此,对于malloc (25),您实际上会收到一个256字节的缓冲区,而malloc (257)将失败并返回一个空指针。

更现实地,一些malloc实现在指针本身中编码分配大小,或者直接使用对应于特定固定大小的类的位模式,或者间接使用散列表或多级trie。如果我没记错的话,Address Sanitizer的内部malloc属于这种类型。对于这样的malloc,至少部分立即分配开销不是来自添加堆管理的元数据,而是来自将分配大小四舍五入到支持的大小类。

其他malloc具有单个词的每个分配标头。 (dlmalloc及其衍生物是受欢迎的例子)。实际的每分配开销通常略大,因为由于标题字,您可以获得支持的分配大小(例如24位,40位,56位,...字节,64位系统上的16字节对齐)。

要记住的一件事是,许多malloc实现放置了大量数据解除分配的对象(尚未返回到操作系统内核),因此{{ 1}}(函数)可以快速找到适当大小的未使用内存区域。特别是对于malloc样式的分配器,这也提供了对最小对象大小的约束。使用解除分配的对象进行堆管理也会导致dlmalloc开销,但它对单个分配的影响难以量化。

答案 1 :(得分:0)

  

比方说,如果我调用malloc(sizeof(int)),请求4个字节,系统(或std库?)将添加多少额外资源来支持内存管理基础架构?我相信应该有一些。否则,当我调用free(ptr)时,系统如何知道要处理多少字节。

这完全是特定于图书馆的。答案可能是从零到任何东西。您的库可以将数据添加到块的前面。有些人将数据添加到块的前面和后面以跟踪覆盖。添加的开销量因库而异。

可以使用表格在库本身内跟踪长度。在这种情况下,可能没有为已分配的内存添加隐藏字段。

库可能只分配固定大小的块。您要求的金额将四舍五入到下一个区块大小。

答案 2 :(得分:0)

通常malloc向上舍入到最小对齐边界,并且通常这不是特殊的小分配,因为应用程序应该将其中许多聚合成单个分配。最小对齐通常基于运行代码的体系结构中的加载指令所需的最大对齐。因此,对于128位SIMD(例如SSE或NEON),最小值为16字节。在实践中,还有一个标题,这使得最小的成本增加一倍。随着SIMD寄存器宽度的增加,malloc没有增加它的保证对齐。

正如所指出的,最小可能的开销是0.虽然指针本身应该在任何合理的分析中计算。在垃圾收集器设计中,必须至少存在一个指向数据的指针。在非GC设计中,必须有一个指向调用free的指针,但是不需要调用它。理论上,理论上可以将一堆指针压缩到更小的空间中,但现在我们分析了指针中位的熵。要点你可能需要指定一些更多的约束才能获得真正可靠的答案。

举例说明,如果需要任意分配和仅释放int大小,可以使用每个int分配一个大块并创建一个链接的索引列表来保存索引。下一个。分配从列表中拉出一个项目,并且取消分配后退一个项目。存在一个约束,即每个分配恰好是int。 (并且该块足够小以使最大索引适合int。)可以通过使用不同的块并在解除分配时搜索指针所在的块来处理多个大小。一些malloc实现对于小的固定大小(例如4,8和16字节)执行类似的操作。

这种方法不会达到零开销,因为需要维护一些数据结构来跟踪块。通过考虑单字节分配的情况来说明这一点。一个块最多可以容纳256个分配,因为这是可以容纳在块中的最大索引。如果我们想要允许比这更多的分配,我们将需要每个块至少一个指针,例如,每256字节开销4或8字节。

还可以使用位图,每个粒度分摊一位,加上该粒度的量化。这是否低开销取决于具体细节。例如。每个字节一位没有量化但在自由映射中占用分配大小的八分之一。通常,这都需要存储分配的大小。

实际上,分配器设计很难,因为大小开销,运行时成本和碎片开销之间的权衡空间很复杂,通常成本差异很大,并且依赖于分配模式。

答案 3 :(得分:0)

指针本身基本上是开销,在某些程序中可能是内存使用的主要驱动程序。

对于某些理论系统和使用,理论上的最小开销可能是sizeof(void*),但CPU,内存和使用模式的组合不太可能存在,因此绝对没有价值。该标准要求malloc返回的内存适合任何数据类型,因此总会有一些开销;以一个已分配块的结尾与下一个已分配块的开头之间的未使用内存的形式,除了极少数情况下,所有内存使用的大小都是块大小的倍数。

malloc / free / realloc的最小实现,假设堆管理器有一个连续的内存块,位于系统内存中的某个位置,表示堆管理器用来引用该原始块的指针是开销(再次sizeof(void*))。人们可以想象一个高度设计的应用程序,它要求整个内存块,从而避免需要额外的跟踪数据。此时,我们有2 * sizeof(void*)开销,一个是堆管理器内部的,另外还有指向一个已分配块的返回指针(理论最小值)。这样一个符合规范的堆管理器不太可能存在,因为它必须有一些从池中分配多个块的方法,这至少意味着跟踪其池中的哪些块正在使用中。

避免开销的一种方案涉及使用大于应用程序可用的物理或逻辑内存的指针大小。可以将一些信息存储在那些未使用的位中,但如果它们的数量超过处理器字大小,它们也将计为开销。通常,只使用一个充满位的手,并且那些用于识别内存中的哪个内存池来自内存。后者暗示了指向池的额外开销。这将我们带到了真实世界的系统,其中堆管理器实现被调整为操作系统,硬件架构和典型的使用模式。

大多数托管实现(托管= =在OS上运行)在c运行时初始化阶段从操作系统请求一个或多个内存块。 OS内存管理调用在时间和空间上都很昂贵。操作系统拥有自己的内存管理器,具有自己的开销集,由自己的设计标准和使用模式驱动。因此,c-runtime堆管理器会尝试限制进入操作系统内存管理器的调用次数,以减少平均调用malloc()free()的延迟。当第一次调用malloc时,大多数请求OS中的第一个块,但这通常发生在c-runtime初始化代码中的某个点上。第一个块通常是系统页面大小的低倍,可以比初始malloc()调用中请求的大小大一个或多个数量级。

此时很明显,堆管理器开销非常流畅且难以量化。在典型的现代系统中,堆管理器必须跟踪从OS分配的多个内存块,当前在每个块中为应用程序分配了多少字节,并且可能是从块变为零以来经过了多少时间。然后是从每个块中跟踪分配的开销。