c – 为什么内存分配器没有主动将释放的内存返回给操作系统?

是的,这可能是你第三次看到这段代码,因为我问了另外两个关于它的问题(thisthis)..
代码很简单:

#include <vector>
int main() {
    std::vector<int> v;
}

然后我在Linux上使用Valgrind构建并运行它:

g++ test.cc && valgrind ./a.out
==8511== Memcheck, a memory error detector
...
==8511== HEAP SUMMARY:
==8511==     in use at exit: 72,704 bytes in 1 blocks
==8511==   total heap usage: 1 allocs, 0 frees, 72,704 bytes allocated
==8511==
==8511== LEAK SUMMARY:
==8511==    definitely lost: 0 bytes in 0 blocks
==8511==    indirectly lost: 0 bytes in 0 blocks
==8511==      possibly lost: 0 bytes in 0 blocks
==8511==    still reachable: 72,704 bytes in 1 blocks
==8511==         suppressed: 0 bytes in 0 blocks
...
==8511== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

在这里,Valgrind报告没有内存泄漏,即使有1个alloc和0个free.

答案here指出C标准库使用的分配器不一定会将内存返回给操作系统 – 它可能会将它们保留在内部缓存中.

问题是:

1)为什么要将它们保存在内部缓存中?如果是速度,它怎么更快?是的,操作系统需要维护一个数据结构来跟踪内存分配,但是这个缓存的维护者也需要这样做.

2)如何实施?因为我的程序a.out已经终止,所以没有其他进程可以维护这个内存缓存 – 或者,有没有?

编辑:问题(2) – 我见过的一些答案建议“C运行时”,这是什么意思?如果“C runtime”是C库,但是库只是坐在磁盘上的一堆机器代码,那么它不是一个正在运行的进程 – 机器代码要么链接到我的a.out(静态库,.a)或者在运行期间(共享对象,.so)在a.out的过程中调用.

最佳答案
澄清

首先,一些澄清.你问:…我的程序a.out已经终止,没有其他进程维护这个内存缓存 – 或者,是否有一个?

我们所讨论的一切都在一个进程的生命周期内:进程总是在退出时返回所有已分配的内存.没有缓存超过process1.即使没有运行时分配器的任何帮助,也会返回内存:操作系统只是在进程终止时“收回”.因此,正常分配的终止应用程序不会发生系统范围的泄漏.

现在Valgrind报告的是在进程终止时使用的内存,但是在操作系统清理所有内容之前.它适用于运行时库级别,而不是OS级别.所以它说“嘿,当程序完成时,有72,000个字节没有返回到运行时”,但未说明的含义是“这些分配将很快被OS清理”.

相关问题

显示的代码和Valgrind输出与名称问题并不完全相关,所以让我们将它们分开.首先,我们将尝试回答您提出的有关分配器的问题:为什么它们存在以及为什么它们通常不会立即将释放的内存返回给操作系统,而忽略了示例.

您询问:

1) Why keep them in an internal cache? If it is for speed, how is it
faster? Yes, the OS needs to maintain a data structure to keep track
of memory allocation, but this the maintainer of this cache also needs
to do so.

这有两个问题合二为一:一个是为什么根本没有使用userland运行时分配器,然后另一个是(可能是?)为什么这些分配器在释放时不会立即将内存返回给OS.它们是相关的,但让我们一次解决它们.

为什么运行时分配器存在

为什么不依靠OS内存分配例程?

>许多操作系统,包括大多数Linux和其他类Unix操作系统,根本没有OS系统调用来分配和释放任意内存块. Unix-alikes提供的brk只能增长或缩小一个连续的内存块 – 你无法“释放”任意的早期分配.它们还提供mmap,允许您独立分配和释放内存块,但这些分配在PAGE_SIZE粒度上,在Linux上为4096字节.因此,如果您需要32个字节的请求,如果您没有自己的分配器,则必须浪费4096 – 32 == 4064个字节.在这些操作系统上,您实际上需要一个单独的内存分配运行时,它将这些粗粒度工具转换为能够有效分配小块的内容.

Windows有点不同.它具有HeapAlloc调用,它是“OS”的一部分,并且提供了类似malloc的功能,可以分配和释放任意大小的内存块.有了一些编译器,malloc就被实现为HeapAlloc的一个薄包装器(在最近的Windows版本中,这个调用的性能有了很大提高,这使得这个可行).尽管如此,虽然HeapAlloc是操作系统的一部分,但它并没有在内核中实现 – 它也主要在用户模式库中实现,管理一个空闲和已用块的列表,偶尔的内核调用可以从中获取大块内存.核心.所以它主要是另一种伪装的malloc,它所持有的任何内存也不能用于任何其他进程.
>表现!即使有适当的内核级调用来分配任意内存块,对内核的简单开销往返通常是几百纳秒或更长.另一方面,经过良好调整的malloc分配或免费通常只有十几条指令,并且可以在10ns或更短的时间内完成.最重要的是,系统调用不能“信任他们的输入”,因此必须仔细验证从用户空间传递的参数.在free的情况下,这意味着它会检查用户是否传递了有效的指针!大多数运行时免费实现只是崩溃或无声地破坏内存,因为没有责任保护进程本身.
>更紧密地链接到语言运行时的其余部分.用于在C中分配内存的函数,即new,malloc和friends,是语言定义的一部分.然后将它们作为实现语言其余部分的运行时的一部分来实现它们是完全自然的,而不是大部分与语言无关的操作系统.例如,语言可能对各种对象具有特定的对齐要求,这可以通过语言感知分配器来最好地处理.对语言或编译器的更改也可能意味着对分配例程进行必要的更改,并且希望更新内核以适应您的语言功能将是一个艰难的要求!

为什么不将内存返回给操作系统

您的示例没有显示它,但是您询问并且如果您编写了不同的测试,您可能会发现在分配然后释放一堆内存后,您的进程驻留设置大小和/或操作系统报告的虚拟大小可能不会免费后减少.也就是说,即使你已经释放它,这个过程似乎仍然存在于内存中.事实上,许多malloc实现都是如此.首先,请注意,这本身并不是泄漏 – 未分配的内存仍然可用于分配它的进程,即使不是其他进程也是如此.

他们为什么这样做?以下是一些原因:

>内核API使其变得困难.对于老式的brk和sbrk system calls,除非碰巧是在从brk或sbrk分配的最后一个块的末尾,否则返回释放的内存是不可行的.这是因为这些调用提供的抽象是一个大的连续区域,您只能从一端扩展.你不能从中间回传内存.而不是试图支持所有释放的内存恰好位于brk区域末端的异常情况,大多数分配器甚至都不打扰.

mmap调用更灵活(这个讨论通常也适用于VirtualAlloc是等效的mmap的Windows),允许你至少以页面粒度返回内存 – 但即便这样也很难!在释放属于该页面的所有分配之前,您无法返回页面.取决于可能常见或不常见的应用程序的大小和分配/自由模式.它运行良好的情况是大型分配 – 大于一页.在这里你可以保证能够释放大部分的分配,如果它是通过mmap完成的,实际上一些现代分配器直接从mmap满足大分配,并用munmap将它们释放回操作系统.对于glibc(以及扩展的C分配运算符),您甚至可以控制this threshold

M_MMAP_THRESHOLD
  For allocations greater than or equal to the limit specified
  (in bytes) by M_MMAP_THRESHOLD that can't be satisfied from
  the free list, the memory-allocation functions employ mmap(2)
  instead of increasing the program break using sbrk(2).

  Allocating memory using mmap(2) has the significant advantage
  that the allocated memory blocks can always be independently
  released back to the system.  (By contrast, the heap can be
  trimmed only if memory is freed at the top end.)  On the other
  hand, there are some disadvantages to the use of mmap(2):
  deallocated space is not placed on the free list for reuse by
  later allocations; memory may be wasted because mmap(2)
  allocations must be page-aligned; and the kernel must perform
  the expensive task of zeroing out memory allocated via
  mmap(2).  Balancing these factors leads to a default setting
  of 128*1024 for the M_MMAP_THRESHOLD parameter.

因此,默认情况下,运行时将直接从OS分配128K或更多的分配,并在空闲时释放回操作系统.因此,有时您会看到您可能期望的行为始终如此.
>表现!每个内核调用都很昂贵,如上面的其他列表中所述.稍后将需要由进程释放的内存以满足另一个分配.而不是试图将它返回到操作系统,这是一个相对重量级的操作,为什么不将它保留在一个免费列表中以满足未来的分配?正如手册页条目中所指出的,这也避免了将内核返回的所有内存清零的开销.它还提供了良好缓存行为的最佳机会,因为该过程不断重用地址空间的相同区域.最后,它避免了由munmap强加的TLB刷新(并且可能通过brk缩小).
>不返回内存的“问题”对于在某个时刻分配一堆内存的长期进程来说是最糟糕的,释放它然后再也不会分配那么多内存.即,分配高水位线的过程大于其长期典型分配量.但是,大多数流程都不遵循这种模式.进程通常释放大量内存,但以一定速率分配,使其总内存使用量不变或可能增加.具有“大而小”实时大小模式的应用程序可能是force the issue with malloc_trim.
>虚拟内存有助于缓解此问题.到目前为止,我一直在抛弃像“已分配的内存”这样的术语而没有真正定义它的含义.如果一个程序分配然后释放2 GB的内存然后无所事事,它是否会浪费2 GB的实际DRAM插入你的主板某处?可能不是.当然,它在您的进程中使用2 GB的虚拟地址空间,但虚拟地址空间是按进程进行的,因此不会直接从其他进程中获取任何内容.如果进程实际上在某个时刻写入了内存,它将被分配物理内存(是的,DRAM) – 在释放后,你 – 按照定义 – 不再使用它.此时,OS可以通过用于其他人来回收这些物理页面.

现在这仍然要求你有交换吸收脏的未使用的页面,但是一些分配器是聪明的:他们可以发出一个madvise(..., MADV_DONTNEED)调用告诉操作系统“这个范围没有任何用处,你不必保留它交换中的内容“.它仍然将虚拟地址空间映射到进程中并稍后可用(零填充),因此它比munmap和后续mmap更有效,但它避免了无意义地交换释放的内存区域到swap.2.

演示代码

正如在this answer中指出的那样,你的测试用向量< int>是不是真的测试任何东西,因为一个空的,未使用的std :: vector< int>只要你使用一些最低级别的优化,v甚至不会是create the vector object.即使没有优化,也不会发生分配,因为大多数向量实现在第一次插入时分配,而不是在构造函数中分配.最后,即使你使用一些不寻常的编译器或库来进行分配,它也会用于少量字节,而不是Valgrind报告的~72,000字节.

你应该做这样的事情来实际看到矢量分配的影响:

#include <vector>

volatile vector<int> *sink;

int main() {
    std::vector<int> v(12345678);
    sink = &v;
}

这导致actual allocation and de-allocation.然而,它不会改变Valgrind输出,因为在程序退出之前正确地释放了向量分配,因此就Valgrind而言没有问题.

在较高的层面上,Valgrind基本上将事物分类为“明确的泄漏”和“退出时没有释放”.前者发生在程序不再引用指向它所分配的内存的指针时.它无法释放这样的记忆,因此泄露了它.在退出时尚未释放的内存可能是“泄漏” – 即应该已被释放的对象,但它也可能只是开发人员知道将占用程序长度的内存,因此不需要显式释放(因为全局变量的破坏顺序问题,特别是当涉及共享库时,即使你想要,也可能很难可靠地释放与全局或静态对象相关的内存).

1在某些情况下,某些特意特殊分配可能会比该过程更长,例如共享内存和内存映射文件,但这与普通C分配无关,您可以为了讨论的目的忽略它.

2最近的Linux内核也有特定于Linux的MADV_FREE,它似乎与MADV_DONTNEED具有相似的语义.

转载注明原文:c – 为什么内存分配器没有主动将释放的内存返回给操作系统? - 代码日志