【问题标题】:Are some allocators lazy?有些分配器是懒惰的吗?
【发布时间】:2021-12-18 07:21:46
【问题描述】:

我在 Linux 中编写了一个 malloc 内存的 C 程序,循环运行,TOP 没有显示任何内存消耗。

然后我对该内存做了一些处理,TOP 确实显示了内存消耗。

当我 malloc 时,我真的“获取内存”,还是有一个“惰性”内存管理,只有在我使用它时才给我内存?

(还有一个选项是TOP在我使用的时候才知道内存消耗,所以我不确定这个..)

谢谢

【问题讨论】:

    标签: linux memory-management malloc


    【解决方案1】:

    在 Linux 上,malloc 使用 sbrk() 或 mmap() 请求内存 - 无论哪种方式,您的地址空间都会立即扩展,但 Linux 在第一次写入相关页面之前不会分配实际的物理内存页面。您可以在 VIRT 列中看到地址空间的扩展,而在 RES 中看到实际的物理内存使用情况。

    【讨论】:

    • windows也一样吗?
    • 我不熟悉 Windows 的功能,抱歉。
    • bdonlan: 正确,但他应该注意 fork 的影响“ * 孩子不会继承其父母的内存锁 (mlock(2), mlockall(2))。” 这将如何大多数应用程序在他查看顶部时的负载
    • 页表中发生了什么?是否有特殊位表示它不存在,但也不在磁盘上(即未分配)?
    • @flow2k 谈论 x86:在页表中只有一个存在位。如果未设置,CPU 将忽略所有其他位并发出缺页异常。然后,操作系统可以调查这些其他位以弄清楚要做什么,或者它可以查找内部结构,或者可以将两者结合起来。
    【解决方案2】:

    这开始有点偏离主题(然后我会将其与您的问题联系起来),但是发生的事情类似于您在 Linux 中分叉一个进程时发生的事情。分叉时有一种称为写时复制的机制,它只在内存也被写入时为新进程复制内存空间。这样,如果分叉的进程 exec 立即成为一个新程序,那么您就节省了复制原始程序内存的开销。

    回到你的问题,这个想法是相似的。正如其他人指出的那样,请求内存会立即为您提供虚拟内存空间,但实际页面仅在写入时分配。

    这样做的目的是什么?它基本上使 mallocing 内存成为或多或少的恒定时间操作 Big O(1) 而不是 Big O(n) 操作(类似于 linux 调度程序传播它的方式而不是在一个大块中进行)。

    为了证明我的意思,我做了以下实验:

    rbarnes@rbarnes-desktop:~/test_code$ time ./bigmalloc 
    
    real    0m0.005s
    user    0m0.000s
    sys 0m0.004s
    rbarnes@rbarnes-desktop:~/test_code$ time ./deadbeef 
    
    real    0m0.558s
    user    0m0.000s
    sys 0m0.492s
    rbarnes@rbarnes-desktop:~/test_code$ time ./justwrites 
    
    real    0m0.006s
    user    0m0.000s
    sys 0m0.008s
    

    bigmalloc 程序分配了 2000 万个整数,但对它们不做任何事情。 deadbeef 将一个 int 写入每个页面,导致 19531 次写入,而 justwrites 分配 19531 个 int 并将它们清零。如您所见, deadbeef 的执行时间比 bigmalloc 长约 100 倍,比 justwrites 长约 50 倍。

    #include <stdlib.h>    
    
    int main(int argc, char **argv) {
    
    int *big = malloc(sizeof(int)*20000000); // allocate 80 million bytes
    
    return 0;
    
    }
    

    .

    #include <stdlib.h>    
    
    int main(int argc, char **argv) {
    
    int *big = malloc(sizeof(int)*20000000); // allocate 80 million bytes
    
    // immediately write to each page to simulate all at once allocation
    
    // assuming 4k page size on 32bit machine
    
    for ( int* end = big + 20000000; big < end; big+=1024 ) *big = 0xDEADBEEF ;    
    
    return 0;
    
    }
    

    .

    #include <stdlib.h>
    
    int main(int argc, char **argv) {
    
    int *big = calloc(sizeof(int),19531); // number of writes
    
    return 0;
    }
    

    【讨论】:

    【解决方案3】:

    是的,除非您触摸它,否则内存不会映射到您的内存空间。 mallocing 内存只会设置分页表,以便他们知道当您在分配的内存中遇到页面错误时,应该映射内存。

    【讨论】:

      【解决方案4】:

      您是否使用编译器优化?也许优化器已经删除了分配,因为您没有使用分配的资源?

      【讨论】:

      • 谢谢 Ryan,我确实用反汇编程序查看了二进制文件,并且有“malloc”调用。
      • +1 反对反对票。这是对问题的一个很好的答案。
      • 编译器无法删除没有可见实现或可能有副作用的函数。
      • @BeeOnRope:编译器已经相当明显地删除了对 memset(0) 的调用,因为从抽象机器的角度来看,这是一个无操作的缓冲区,因为它永远不会观察到写入的值.标准中定义的任何功能在理论上都受到这种处理。另请参阅 C++ 即将发布的constexpr new
      • @phil 同意,自从我写了这篇文章以来,我学到了很多东西。更重要的是,编译器肯定会消除 malloc 调用。我原以为它们会被认为是不透明的(实际上它们可以被插入,因此在某些环境中可以“观察到”)——但显然情况并非如此。
      【解决方案5】:

      该功能称为 overcommit - 内核通过增加数据段大小“承诺”您的内存,但不为其分配物理内存。当您在该新空间中触摸一个地址时,进程页面错误会进入内核,然后内核会尝试将物理页面映射到该地址。

      【讨论】:

        【解决方案6】:

        是的,请注意VirtualAlloc 标志,

        MEM_RESERVE
        MEM_COMMIT
        

        .

        嘿,但是对于 Linux 或任何POSIX/BSD/SVR# 系统,vfork() 已经存在了很长时间并提供了类似的功能。

        vfork() 函数不同于 fork() 仅在子进程中 可以共享代码和数据 调用进程(父进程)。这 显着加速克隆活动 有风险的完整性 如果 vfork() 被滥用,父进程。

        出于任何目的使用 vfork() 除了作为即时的前奏 从 exec 调用函数 不建议家人或 _exit()。

        vfork() 函数可用于 没有完全创建新流程 复制旧地址空间 过程。如果一个分叉的进程只是 将调用 exec,数据空间 通过从父级复制到子级 不使用 fork()。这是 在分页中效率特别低 环境,制作 vfork() 特别有用。依赖于 父数据空间的大小, vfork() 可以给出显着的 对 fork() 的性能改进。

        【讨论】:

          猜你喜欢
          • 2018-01-28
          • 1970-01-01
          • 2017-01-21
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多