gpt4 book ai didi

c++ - 为什么内存分配器不主动将释放的内存返回给操作系统?

转载 作者:行者123 更新时间:2023-12-04 16:58:04 26 4
gpt4 key购买 nike

是的,这可能是您第三次看到此代码,因为我还问了另外两个问题(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++运行时”是C++库,但该库只是磁盘上的一堆机器代码,则它不是正在运行的进程-机器代码要么链接到我的 a.out(静态库, .a),要么被调用在 .so的过程中(共享对象 a.out)运行时。

最佳答案

澄清度

首先,进行一些澄清。您问:...我的程序a.out已经终止,没有其他进程正在维护此内存缓存-还是有一个?

我们所讨论的一切都在单个进程的生存期内:该进程退出时始终返回所有分配的内存。没有比进程1持久的缓存。即使没有运行时分配器的任何帮助,也将返回内存:进程终止时,操作系统仅“收回”该内存。因此,正常分配的终止应用程序不会发生系统范围的泄漏。

现在,Valgrind报告的是进程终止时但操作系统清理所有内容之前正在使用的内存。它在运行时库级别而不是操作系统级别工作。因此,它的意思是“嘿,当程序完成时,还有72,000个字节尚未返回到运行时”,但未说明的含义是“这些分配将很快由OS清除”。

基本问题

所示的代码和Valgrind输出与名义问题并没有很好的关联,因此让我们将它们分开。首先,我们将尝试回答您有关分配器的问题:它们为什么存在以及为什么它们通常不立即将释放的内存返回给OS,而忽略了该示例。

你问:

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一样提供brk,它只能增加或减少一个连续的内存块-您无法“释放”任意较早的分配。它们还提供了mmap,它允许您独立分配和释放内存块,但是它们以PAGE_SIZE粒度(在Linux上为4096字节)进行分配。因此,如果您希望请求32个字节,那么如果您没有自己的分配器,则必须浪费4096 - 32 == 4064字节。在这些操作系统上,您实际上需要一个单独的内存分配运行时,它将这些粗粒度的工具转换为能够有效分配小块的东西。

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

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

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

    他们为什么这样做?原因如下:
  • 内核API使其很难。对于老式的brksbrk system calls,返回释放的内存根本不可行,除非它恰好位于从brksbrk分配的最后一个块的末尾。这是因为这些调用提供的抽象是一个大的连续区域,您只能从一端延伸。您不能从内存中途交出内存。大多数分配器都没有理会这种不寻常的情况,即所有释放的内存恰好在brk区域的末尾。
    mmap调用更加灵活(此讨论通常也适用于VirtualAllocmmap等效的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或更多的分配,并免费释放回OS。因此,有时您会始终看到预期的行为。
  • 性能!如上面其他列表中所述,每个内核调用都很昂贵。稍后将需要由进程释放的内存,以满足另一分配。与其尝试将其返回给操作系统,而是一项相对繁重的操作,不如不将其保留在空闲列表中以满足将来的分配?正如手册页条目中指出的那样,这还避免了将内核返回的所有内存清零的开销。由于进程不断地重复使用地址空间的相同区域,因此它也提供了良好的缓存行为的最佳机会。最后,它避免了由munmap(可能通过brk缩小)强加的TLB刷新。
  • 对于长时间运行的进程,不返回内存的“问题”是最糟糕的,这些进程会在某个时刻分配一堆内存,释放它,然后再也不分配那么多。即,分配高水位线大于其长期典型分配量的过程。但是,大多数过程只是不遵循这种模式。进程通常会释放大量内存,但是分配的速率应使它们的整体内存使用量恒定或可能增加。确实具有“先有先有”模式的应用程序可能会使用force the issue with malloc_trim
  • 虚拟内存有助于缓解此问题。到目前为止,我一直在围绕诸如“分配的内存”之类的术语而未真正定义其含义。如果程序先分配然后释放2 GB的内存,然后闲着无所事事,那是不是在浪费2 GB的实际DRAM插入主板的某处?可能不是。当然,它在您的进程中使用2 GB的虚拟地址空间,但是虚拟地址空间是每个进程的,因此不会直接从其他进程中获取任何东西。如果该进程确实在某个时刻写入了内存,则会为它分配物理内存(是的,是DRAM)-释放它之后,根据定义,您将不再使用它。此时,操作系统可能会通过供他人使用来回收这些物理页面。

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

  • 示范代码

    正如 this answer中指出的那样,您使用 vector<int>进行的测试实际上并未测试任何内容,因为只要您使用了最低限度的优化级别,一个空的,未使用的 std::vector<int> v甚至都不会使用 create the vector object。即使没有优化,也不会发生分配,因为大多数 vector实现都是在第一次插入时分配的,而不是在构造函数中的。最后,即使您使用的是一些不常见的编译器或库来进行分配,它也会占用很少的字节,而不是Valgrind报告的〜72,000字节。

    您应该执行以下操作才能实际看到 vector 分配的影响:
    #include <vector>

    volatile vector<int> *sink;

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

    结果为 actual allocation and de-allocation。但是,由于将在程序退出之前正确释放 vector 分配,因此不会更改Valgrind的输出,因此就Valgrind而言,这没有问题。

    在较高级别上,Valgrind基本上将事物分类为“确定的泄漏”和“在导出未释放”。前者发生在程序不再引用指向其分配的内存的指针时。它无法释放此类内存,因此泄漏了它。退出时尚未释放的内存可能是“泄漏”,即应该释放的对象,但也可能只是开发人员知道可以使用程序长度的内存,因此不需要被明确释放(由于全局变量的销毁顺序问题,尤其是在涉及共享库时,即使您愿意,也很难可靠地释放与全局或静态对象关联的内存)。

    1在某些情况下,某些故意的特殊分配可能会使该过程无法使用,例如共享内存和内存映射文件,但这与普通的C++分配无关,在本讨论中您可以忽略它。

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

    关于c++ - 为什么内存分配器不主动将释放的内存返回给操作系统?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/45538993/

    26 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com