C++ 库实现如何在程序退出时分配内存但不释放内存?

How does a C++ library implementation allocate memory but not free it when the program exits?

代码相当简单:

#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)

这里没有内存泄漏,即使有 1 个分配和 0 个空闲。这个 quotes this paragraph from Valgrind's FAQ的答案解释一下-

Many implementations of the C++ standard libraries use their own memory pool allocators. Memory for quite a number of destructed objects is not immediately freed and given back to the OS, but kept in the pool(s) for later re-use.

我的主要问题是:

C++ 库实现是如何实现的?它是否在后台保留一个单独的进程来处理来自其标准模板的所有分配请求,以便当程序退出时(此处为 a.out),内存不会立即返回给 OS?如果是这样,它什么时候会返回,我如何检查该过程是否确实存在?如果不是,幕后的"magic"是什么?

另一个问题:

分配了 71 KB。为什么是这个数字?

谢谢:)

首先,您没有用未使用的 vector 测试任何东西。编译器很聪明,gccclang-O2 都将上面的代码编译为空的 main()(除了单个 xor eax, eax 来设置 return 值。请参阅程序集 here。此外,大多数 vector 实现(包括 gccclang)的默认构造函数甚至不会分配任何内容 - 它会等到第一个元素被添加,然后再进行昂贵的分配步骤。

要获得更具体的结果,请分配一个 BIG 向量(以便您可以将其与噪音区分开来)并将其传递给另一个翻译单元(或在单独的 .cpp 文件中定义)中的方法,如下所示:

#include <vector>

void sink(std::vector<int>& v);

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

现在当您检查程序集时,您会看到它是 actually doing something

因此,您看到的 Valgrind 报告的 ~72,000 字节与您的 std::vector<int> v 无关,您可能会看到相同的数字,但 main 完全为空。

问题的想法和引用的文档仍然与该问题不同,我将在下面回答。

所有内存一般在程序退出时释放回OS,强制执行的是OS,而不是标准库. OS 只是清除进程使用的所有资源,包括未共享的内存分配。当 Valgrind 引用 "in use at exit" 时,它是在 OS 清理发生之前谈论的,因为这是您想知道的,看看您是否忘记释放任何东西。

您不需要任何单独的进程来处理它。它是通过让 Valgrind 跟踪 mallocfree 调用以及也许其他一些标准分配例程来实现的。

您从常见问题解答中引用的关于许多使用 "use their own memory pool allocators" 的标准库的评论指的是这样一种想法,即标准库可能会在那些调用已知分配调用的层之上使用另一个缓存分配层,例如mallocoperator new 最初 当需要内存时,但是当内存被取消分配时,它会在内部将其保存在某个列表中,而不是调用相应的取消分配常规(例如 freedelete)。

在随后的分配中,它将优先使用其内部列表中的内容,而不是返回到标准方法(如果列表已用尽,则必须调用标准例程)。这将使它对 Valgrind 不可见,Valgrind 会认为应用程序仍然 "in use" 内存。

由于旧版本的 C++ 中 std::allocator 东西的一些无用的定义,这没有被大量使用,我不同意 "many" 标准库使用这种类型的池默认分配器 - 至少在今天:我实际上不知道任何在主要标准库实现之间不再这样做,尽管过去有些人知道。但是,allocator 参数是每个容器的模板参数 class,因此最终用户也可以执行此自定义,特别是因为 allocator 接口在更新的标准中得到了改进。

这种池化分配器在实践中的巨大优势是:(a) 对容器使用线程局部、固定大小的分配,因为所有包含的对象都具有相同的大小,以及 (b) 允许分配器在一次操作中释放所有内容容器被销毁而不是逐个元素地释放。

您引用的文档有点令人困惑,因为它谈到(不是)将内存重新调整到 OS - 但它实际上应该说 "retuning to the standard allocation routines" . Valgrind 不需要将内存 returned 到 OS 来将其视为已释放 - 它挂钩所有标准例程并知道您何时在该级别释放。标准例程本身大量 "cache" 如上所述分配内存(这很常见,不像分配器例程缓存不常见)所以如果 Valgrind 需要内存 returned 到 OS 它会在报告方面毫无用处 "allocated memory at exit"。

我想你误会了。如果应用程序终止,内存将返回给 os。但是内存并没有还给os,只是因为对象被销毁了

How does the C++ library implementation achieve that?

没有。 valgrind 信息已过时,我认为任何现代 C++ 实现都不会这样做。

Does it keep around a separate process in the background that handles all allocation requests from its standard templates, so that when the program exits (a.out here), the memory is not immediately given back to the OS?

不对,你误会了。 valgrind 文档并不是在谈论保留比进程更长的内存。它只是在谈论在进程中保留内存池,以便进程分配和释放的内存保存在池中并稍后重用(由同一进程!),而不是立即调用 free 。但是现在没有人为 std::allocator 这样做,因为 std::allocator 需要是通用的并且在所有情况下都表现得相当好,并且一个好的 malloc 实现无论如何应该满足这些需求。用户也很容易用 tcmalloc 或 jemalloc 等替代方案覆盖默认系统 malloc,因此如果 std::allocator 只是转发到 malloc,那么它会获得替代 malloc 的所有好处。

If so, when will it give back, and how can I check the process indeed exists? If not, what is the "magic" behind the scene?

进程退出时,进程中的所有内存都返回给OS。没有魔法。

但是您看到的分配与此无关。

There is 71 KB allocated. Why this number?

您看到的 72kb 是由 C++ 运行时为其 "emergency exception-handling pool" 分配的。该池用于分配异常对象(例如 bad_alloc 异常),即使 malloc 不能再分配任何东西。我们在启动时预先分配,所以如果 malloc 内存不足,我们仍然可以抛出 bad_alloc 异常。

具体数字来源于这段代码:

       // Allocate the arena - we could add a GLIBCXX_EH_ARENA_SIZE environment
       // to make this tunable.
       arena_size = (EMERGENCY_OBJ_SIZE * EMERGENCY_OBJ_COUNT
                     + EMERGENCY_OBJ_COUNT * sizeof (__cxa_dependent_exception));
       arena = (char *)malloc (arena_size);

https://gcc.gnu.org/git/?p=gcc.git;a=blob;f=libstdc%2B%2B-v3/libsupc%2B%2B/eh_alloc.cc;h=005c28dbb1146c28715ac69f013ae41e3492f992;hb=HEAD#l117

valgrind 的较新版本知道这个紧急 EH 池,并在进程退出之前调用一个特殊函数来释放它,这样你就看不到 in use at exit: 72,704 bytes in 1 blocks。这样做是因为太多人不明白仍在使用(并且仍然可以访问)的内存不是泄漏,并且人们一直在抱怨它。所以现在 valgrind 释放了它,只是为了阻止人们抱怨。如果不是 运行 在 valgrind 下,池不会被释放,因为这样做是不必要的(无论如何 OS 都会在进程退出时回收它)。