我注意到Linux中有关top
报告的内存使用情况(RES)的一些有趣行为。我附加了以下程序,它在堆上分配了几百万个对象,每个对象都有一个大约1千字节的缓冲区。指向这些对象的指针由std::list
或std::vector
跟踪。我注意到的有趣行为是,如果我使用std::list
,top
报告的内存使用量在睡眠期间永远不会改变。但是,如果我使用std::vector
,则在这些睡眠期间内存使用率将降至接近0。
我的测试配置是:
Fedora Core 16
内核3.6.7-4
g ++版本4.6.3
我已经知道的事情:
1. std :: vector将根据需要重新分配(大小加倍)
2. std :: list(我相信)一次分配其元素1
3. std :: vector和std :: list都默认使用std :: allocator来获取它们的实际内存
该计划没有泄漏; valgrind宣称不可能有泄漏。
我感到困惑的是:
1. std :: vector和std :: list都使用std :: allocator。即使std :: vector正在进行批量重新分配,std :: allocator也不会以几乎相同的方式将内存分发给std :: list和std :: vector?毕竟这个程序是单线程的
2.我在哪里可以了解Linux内存分配的行为。我听说过关于Linux保持RAM分配给进程的声明,即使它释放了它,但我不知道这种行为是否得到保证。为什么使用std :: vector会影响这种行为呢?
非常感谢您阅读本文;我知道这是一个相当模糊的问题。我在这里寻找的'答案'是这个行为是'定义'还是我可以找到它的文档。
#include <string.h>
#include <unistd.h>
#include <iostream>
#include <vector>
#include <list>
#include <iostream>
#include <memory>
class Foo{
public:
Foo()
{
data = new char[999];
memset(data, 'x', 999);
}
~Foo()
{
delete[] data;
}
private:
char* data;
};
int main(int argc, char** argv)
{
for(int x=0; x<10; ++x)
{
sleep(1);
//std::auto_ptr<std::list<Foo*> > foos(new std::list<Foo*>);
std::auto_ptr<std::vector<Foo*> > foos(new std::vector<Foo*>);
for(int i=0; i<2000000; ++i)
{
foos->push_back(new Foo());
}
std::cout << "Sleeping before de-alloc\n";
sleep(5);
while(false == foos->empty())
{
delete foos->back();
foos->pop_back();
}
}
std::cout << "Sleeping after final de-alloc\n";
sleep(5);
}
答案 0 :(得分:3)
释放内存是在“块”的基础上完成的。很有可能当你使用list时,内存会被分成几个小小的碎片。
当你使用向量分配时,所有元素都存储在一个大块中,因此内存释放代码很容易说“Golly,我在这里有一个非常大的自由区域,我将发布它回到操作系统“。在向量增长时,内存分配器进入“大块模式”也是完全可能的,它使用与“小块模式”不同的分配方法 - 比如你分配超过1MB,内存分配代码可能会看到作为开始使用不同策略的好时机,只需要求操作系统获得“完美契合”的内存。这个大块很容易在被释放时释放回操作系统。
如果你要添加一个列表,那么你总是要求一点点,所以分配器使用不同的策略来请求大块,然后给出一小部分。确保块中的所有块都已被释放既困难又耗时,因此分配器可能“不打扰” - 因为可能存在“仍在使用中”的某些区域,然后它可以'无论如何都要被释放。
我还要补充一点,使用“top”作为内存度量并不是一种特别准确的方法,并且非常不可靠,因为它在很大程度上取决于操作系统和运行时库的功能。属于进程的内存可能不是“驻留”,但进程仍然没有释放它 - 它只是“不存在于实际内存中”(而是在交换分区中出现!)
对于你的问题“这是在某处定义的”,我认为这是C / C ++库源码鳕鱼定义它的意义。但它并没有在某种意义上定义,它写的是“这就是它的工作方式,我们保证永远不会改变它”。作为glibc和libstdc ++提供的库不会说,随着新技术和想法的发明,它们将改变malloc,free,new和delete的内部结构 - 有些可能会让事情变得更好,有些可能会让事情变得更糟场景。
正如评论中指出的那样,内存未锁定到进程。如果内核认为内存更好地用于其他东西[并且内核在这里是无所不能的],那么它将从一个正在运行的进程中“窃取”内存并将其交给另一个进程。特别是长时间未被“触摸”的记忆。
答案 1 :(得分:2)
1。 std :: vector和std :: list都使用std :: allocator。即使std :: vector正在进行批量重新分配,也不会是std :: allocator 以几乎相同的方式将内存分发给std :: list和 的std ::载体?毕竟这个程序是单线程的。
嗯,有什么区别?
std::list
逐个分配节点(除了Foo *
之外,每个节点还需要两个指针)。此外,它永远不会重新分配这些节点(这由list
的迭代器失效要求保证)。因此,std::allocator
将从基础机制请求一系列固定大小的块(可能是malloc
,然后将使用sbrk
或mmap
系统调用。这些固定大小的块可能比列表节点大,但如果是这样,它们都将是std::allocator
使用的相同默认块大小。
std::vector
分配一个连续的指针块,没有任何记账开销(这都在向量父对象中)。每当push_back
溢出当前分配时,向量将分配一个新的,更大的块,将所有内容移动到新块,并释放旧块。现在,新块的大小将类似于旧块的大小(或1.6倍,或其他任何大小),这是保持push_back
的分摊的常量时间保证所需的。所以,很快,我希望它要求的大小超过std::allocator
的任何合理的默认块大小。
因此,有趣的交互是不同的:一个介于std::vector
和分配器的底层机制之间,另一个介于std::allocator
本身和底层机制之间。
2。我在哪里可以了解Linux内存分配的行为。我听说过关于Linux保持RAM分配给进程的声明 即使它释放了它,但我不知道这种行为是否存在 保证。为什么使用std :: vector会影响这种行为呢?
您可能会关注以下几个级别:
std::allocator
本身,可以为小分配提供一层缓冲
std::allocator
实现(例如,它可能是malloc
,但是由您的libc实现)在您的特定情况下,我可以想到一个可能的解释,该向量显然释放了比列表更多的内存。
考虑到向量最终只有一个连续的分配,并且许多Foo
s也将被连续分配。这意味着当你释放所有这些内存时,很容易发现大多数底层页面都是真正免费的。
现在考虑列表节点分配与Foo
实例以1:1交错。即使分配器进行了一些批处理,看起来堆可能比std::vector
情况下更加分散。因此,当您释放已分配的记录时,需要做一些工作来确定底层页面现在是否空闲,并且没有特别的理由预期会发生这种情况(除非后续的大型分配鼓励合并堆记录)。
答案 2 :(得分:1)
答案是malloc&#34; fastbins&#34;优化。 std :: list创建微小的(少于64个字节)分配,当它释放它们时,它们实际上没有被释放 - 而是转到fastblock池。 此行为意味着即使清除列表后堆仍保持碎片状态,因此它不会返回到系统。
您可以使用malloc_trim(128 * 1024)强行清除它们。 或者使用mallopt(M_MXFAST,0)来完全禁用fastbins。
如果你真的不再需要记忆,那么我发现第一个解决方案更正确。
答案 3 :(得分:0)
较小的块通过brk并调整数据段和不断的分裂和融合以及更大的块mmap过程受到的影响稍微小一些。更多信息(PDF)
也是ptmalloc源代码。