Lee*_*hai 5 c++ memory-management libstdc++
是的,这可能是你第三次看到这段代码,因为我问了另外两个关于它的问题(这个和这个)..代码很简单:
#include <vector>
int main() {
std::vector<int> v;
}
Run Code Online (Sandbox Code Playgroud)
然后我在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)
Run Code Online (Sandbox Code Playgroud)
在这里,Valgrind报告没有内存泄漏,即使有1个alloc和0个free.
这里的答案指出C++标准库使用的分配器不一定会将内存返回给操作系统 - 它可能会将它们保存在内部缓存中.
问题是:
1)为什么要将它们保存在内部缓存中?如果是速度,它怎么更快?是的,操作系统需要维护一个数据结构来跟踪内存分配,但是这个缓存的维护者也需要这样做.
2)如何实施?因为我的程序a.out已经终止,所以没有其他进程可以维护这个内存缓存 - 或者,有没有?
编辑:问题(2) - 我见过的一些答案建议"C++运行时",这是什么意思?如果"C++运行时"是C++库,但是库只是位于磁盘上的一堆机器代码,那么它不是一个正在运行的进程 - 机器代码要么链接到我的a.out(静态库.a),要么在运行时被调用(共享对象.so)在过程中a.out.
Bee*_*ope 11
首先,一些澄清.你问:...我的程序a.out已经终止,没有其他进程维护这个内存缓存 - 或者,是否有一个?
我们所讨论的一切都在一个进程的生命周期内:进程总是在退出时返回所有已分配的内存.没有缓存超过流程1.即使没有运行时分配器的任何帮助,也会返回内存:操作系统只是在进程终止时"收回".因此,正常分配的终止应用程序不会发生系统范围的泄漏.
现在Valgrind报告的是在进程终止时使用的内存,但是在操作系统清理所有内容之前.它适用于运行时库级别,而不是OS级别.所以它说"嘿,当程序完成时,有72,000个字节没有返回到运行时",但未说明的含义是"这些分配将很快被OS清理".
显示的代码和Valgrind输出与名称问题并不完全相关,所以让我们将它们分开.首先,我们将尝试回答您提出的有关分配器的问题:为什么它们存在以及为什么它们通常不会立即将释放的内存返回给操作系统,而忽略了示例.
您询问:
1)为什么要将它们保存在内部缓存中?如果是速度,它怎么更快?是的,操作系统需要维护一个数据结构来跟踪内存分配,但是这个缓存的维护者也需要这样做.
这有两个问题合二为一:一个是为什么根本没有使用userland运行时分配器,然后另一个是(可能是?)为什么这些分配器在释放时不会立即将内存返回给操作系统.它们是相关的,但让我们一次解决它们.
为什么不依靠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这意味着它会检查用户是否传递了有效的指针!大多数运行时free实现只是崩溃或无声地破坏内存,因为没有责任保护进程本身.new,malloc和朋友,是一个由语言定义的一部分.然后将它们作为实现语言其余部分的运行时的一部分来实现它们是完全自然的,而不是大部分与语言无关的操作系统.例如,语言可能对各种对象具有特定的对齐要求,这可以通过语言感知分配器来最好地处理.对语言或编译器的更改也可能意味着对分配例程进行必要的更改,并且希望更新内核以适应您的语言功能将是一个艰难的要求!您的示例没有显示它,但是您询问并且如果您编写了不同的测试,您可能会发现在分配然后释放一堆内存后,您的进程驻留设置大小和/或操作系统报告的虚拟大小可能不会免费后减少.也就是说,即使你已经释放它,这个过程似乎仍然存在于内存中.事实上,许多malloc实现都是如此.首先,请注意,这本身并不是泄漏 - 未分配的内存仍然可用于分配它的进程,即使不是其他进程也是如此.
他们为什么这样做?以下是一些原因:
内核API使其变得困难.对于旧学校brk和sbrk 系统调用,返回释放的内存根本不可行,除非它恰好位于从brk或分配的最后一个块的末尾sbrk.这是因为这些调用提供的抽象是一个大的连续区域,您只能从一端扩展.你不能从中间回传内存.而不是试图支持所有释放的内存恰好位于brk区域末端的异常情况,大多数分配器甚至都不打扰.
的mmap通话更加灵活(并且这个讨论通常也适用于Windows,其中VirtualAlloc是mmap等效的),可以让你至少回报存储在页面粒度-但即使是难!在释放属于该页面的所有分配之前,您无法返回页面.取决于可能常见或不常见的应用程序的大小和分配/自由模式.它运行良好的情况是大型分配 - 大于一页.在这里你可以保证能够释放大部分的分配,如果它是通过实现的,mmap并且实际上一些现代分配器直接满足大量分配mmap并将它们释放回操作系统munmap.对于glibc(以及扩展的C++分配运算符),您甚至可以控制此阈值:
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.
Run Code Online (Sandbox Code Playgroud)
因此,默认情况下,运行时将直接从OS分配128K或更多的分配,并在空闲时释放回操作系统.因此,有时您会看到您可能期望的行为始终如此.
munmap(并且可能通过收缩通道brk)强加的TLB刷新.malloc_trim.虚拟内存有助于缓解此问题.到目前为止,我一直在抛弃像"已分配的内存 " 这样的术语而没有真正定义它的含义.如果一个程序分配然后释放2 GB的内存,然后无所事事,是否会浪费2 GB的实际DRAM插入你的主板?可能不是.当然,它在您的进程中使用2 GB的虚拟地址空间,但虚拟地址空间是按进程进行的,因此不会直接从其他进程中获取任何内容.如果进程实际上在某个时刻写入了内存,它将被分配物理内存(是的,DRAM) - 在释放后,你 - 按照定义 - 不再使用它.此时,OS可以通过用于其他人来回收这些物理页面.
现在这仍然需要你交换吸收脏的未使用的页面,但是一些分配器是聪明的:他们可以发出一个madvise(..., MADV_DONTNEED)调用告诉操作系统"这个范围没有任何用处,你不必保留其内容在交换".它仍然将虚拟地址空间映射到进程中并稍后可用(零填充),因此它比munmap后续更有效mmap,但它可以避免无意义地交换释放的内存区域进行交换.2
正如在这个答案中所指出的那样,你的测试并vector<int>没有真正测试任何东西,因为只要你使用一些最低级别的优化,一个空的,未使用的std::vector<int> v甚至不会创建矢量对象.即使没有优化,也不会发生vector分配,因为大多数实现在第一次插入时分配,而不是在构造函数中分配.最后,即使你使用一些不寻常的编译器或库来进行分配,它也会用于少量字节,而不是Valgrind报告的~72,000字节.
你应该做这样的事情来实际看到矢量分配的影响:
#include <vector>
volatile vector<int> *sink;
int main() {
std::vector<int> v(12345678);
sink = &v;
}
Run Code Online (Sandbox Code Playgroud)
这导致实际分配和解除分配.但是,它不会改变Valgrind输出,因为在程序退出之前正确地释放了向量分配,因此就Valgrind而言没有任何问题.
在较高的层面上,Valgrind基本上将事物分类为"明确的泄漏"和"退出时没有释放".前者发生在程序不再引用指向它所分配的内存的指针时.它无法释放这样的记忆,因此泄露了它.在退出时尚未释放的内存可能是"泄漏" - 即应该已被释放的对象,但它也可能只是开发人员知道将占用程序长度的内存,因此不需要显式释放(因为全局变量的破坏顺序问题,特别是当涉及共享库时,即使你想要,也可能很难可靠地释放与全局或静态对象相关的内存).
1在某些情况下,某些特意特殊分配可能会比该过程更长,例如共享内存和内存映射文件,但这与普通C++分配无关,您可以为了讨论的目的忽略它.
2最近的Linux内核也具有特定于Linux的内核MADV_FREE,它似乎具有类似的语义MADV_DONTNEED.
| 归档时间: |
|
| 查看次数: |
632 次 |
| 最近记录: |