首页 文章

每个对象内存分配有多少开销? [关闭]

提问于
浏览
0

比方说,如果我调用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

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

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

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

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

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

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

  • 0

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

    正如所指出的,最小可能的开销是0.虽然指针本身应该在任何合理的分析中计算 . 在垃圾收集器设计中,必须至少存在一个指向数据的指针 . 在非GC设计中,必须有一个指向调用 free 的指针,但是有一个指针用于分析指针中的位的熵 . 要点你可能需要指定一些更多的约束来获得一个非常可靠的答案 .

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

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

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

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

  • 0

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

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

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

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

  • 2

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

    这完全是特定于库的 . 答案可能是任何事情零至零 . 您的库可以将数据添加到块的前面 . 有些人将数据添加到块的前面和后面以跟踪覆盖 . 添加的开销量因库而异 .

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

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

相关问题