当使用std::vector和std::list时,Linux内存使用排在最前面
Linux Memory Usage in top when using std::vector versus std::list
我注意到Linux中关于top
报告的内存使用(RES)的一些有趣行为。我附上了下面的程序,它在堆上分配了几百万个对象,每个对象都有一个大约1kb的缓冲区。指向这些对象的指针由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来获取它们的实际内存
4. 程序没有泄漏;Valgrind已经声明不可能有泄漏。
让我困惑的是:
1. std::vector和std::list都使用std::allocator。即使std::vector正在进行批量重新分配,std::allocator不会以几乎相同的方式将内存分配给std::list和std::vector吗?毕竟这个程序是单线程的。
2. 我在哪里可以了解Linux的内存分配行为。我听说过Linux在释放一个进程后仍然保留分配给它的内存,但我不知道这种行为是否得到保证。为什么使用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-allocn";
sleep(5);
while(false == foos->empty())
{
delete foos->back();
foos->pop_back();
}
}
std::cout << "Sleeping after final de-allocn";
sleep(5);
}
内存的释放是在"chunk"的基础上完成的。当你使用list时,很有可能内存被分割成小块。
当你使用vector进行分配时,所有元素都存储在一个大块中,所以内存释放代码很容易说"天哪,我这里有一个非常大的空闲区域,我要把它释放回操作系统"。当向量增长时,内存分配器也完全有可能进入"大块模式",它使用与"小块模式"不同的分配方法—例如,您分配了超过1MB的内存,内存分配代码可能会认为这是开始使用不同策略的好时机,并要求操作系统提供"完美匹配"的内存块。这个大块在被释放时很容易释放回操作系统。
另一方面,如果你正在添加到一个列表中,你会不断地请求一小部分,所以分配器使用一种不同的策略,请求大块,然后分配一小部分。确保块内的所有块都被释放既困难又耗时,所以分配器可能"不麻烦"——因为可能有一些区域"仍在使用",然后无论如何都无法释放。
我还想补充一点,使用"top"作为内存度量并不是一个特别准确的方法,而且非常不可靠,因为它在很大程度上取决于操作系统和运行时库的功能。属于进程的内存可能不是"常驻"的,但是进程仍然没有释放它——它只是没有"存在于实际内存中"(而是在交换分区中!)
对于你的问题"这是在某处定义的吗",我认为这是在C/c++库源代码定义它的意义上。但它的定义并不是某个地方写着"这就是它的工作方式,我们保证永远不会改变它"。作为glibc和libstdc++提供的库不会这么说,它们会随着新技术和新思想的发明而改变malloc、free、new和delete的内部结构——对于给定的场景,有些可能会使事情变得更好,有些可能会使事情变得更糟。
正如在注释中指出的那样,内存没有锁定到进程。如果内核觉得内存用在其他地方会更好(内核在这方面无所不能),那么它会从一个正在运行的进程中"窃取"内存,并将其提供给另一个进程。特别是那些很长时间没有被"触碰"的记忆
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
分配一个连续的指针块,没有记账开销(这都在vector父对象中)。每次push_back
溢出当前分配时,vector将分配一个新的更大的块,将所有内容移动到新块中,并释放旧块。现在,新块的大小将是旧块的两倍(或1.6倍,或其他),因为需要保持push_back
的平摊常数时间保证。所以,很快,我希望它请求的大小超过std::allocator
的任何明智的默认块大小。
所以,有趣的交互是不同的:一个在std::vector
和分配器的底层机制之间,一个在std::allocator
本身和底层机制之间。
2。我在哪里可以了解Linux的内存分配行为。我听说过Linux将RAM分配给进程的说法即使在它释放了它之后,但我不知道这种行为是否保证。为什么使用std::vector会对这种行为产生如此大的影响?
有几个级别你可能会关心:
- 容器自己的分配模式:希望上面描述过
- 请注意,在实际应用中,容器的使用方式同样重要
-
std::allocator
本身,它可以为小的分配提供一层缓冲- 我认为这不是标准所要求的,所以这是具体到你的实施
- 底层分配器,这取决于您的
std::allocator
实现(例如,它可以是malloc
,但它是由您的libc实现的) - 内核使用的VM方案,以及它与最终使用 的任何系统调用(3)的交互
在你的特殊情况下,我可以想到一个可能的解释,为什么向量明显比列表释放更多的内存。
考虑该向量最终以单个连续分配结束,并且许多Foo
s也将连续分配。这意味着,当您释放所有这些内存时,很容易发现大多数底层页面确实是空闲的。
现在考虑列表节点分配与Foo
实例1:1交错。即使分配器做了一些批处理,堆看起来也可能比std::vector
的情况更加碎片化。因此,当您释放已分配的记录时,需要做一些工作来确定底层页面现在是否空闲,并且没有特别的理由期望会发生这种情况(除非随后的大分配鼓励堆记录合并)。
答案是malloc "fastbins"优化。list创建了很小的(小于64字节)分配,当它释放它们时,它们实际上并没有被释放——而是进入了快速块池。这种行为意味着即使在列表被清除后堆仍然是碎片化的,因此它不会返回系统。
您可以使用malloc_trim(128*1024)来强制清除它们。或者使用mallopt(M_MXFAST, 0)来完全禁用fastbin。
我发现第一个解决方案是更正确的,如果你调用它时,你真的不需要内存了。
较小的块通过brk和调整数据段并不断分裂和融合,而较大的块mmap的过程较少受到干扰。更多信息(PDF)
- 多个文件的内存分配错误"在抛出 'std :: bad_alloc' what (): std :: bad_alloc 的实例后终止调用" [C++]
- 当指向对象的指针作为参数传递给 std::thread 时,内存可见性
- 为什么这个 std::queue/指向结构的指针列表直到 List.Size() == 0 才释放内存?
- 使 std::vector 分配对齐内存的现代方法
- std::unordered_map析构函数不释放内存?
- std::p romise::set_value() 和 std::future::wait() 是否提供内存围栏?
- 如何为 std::vector 分配内存,然后稍后为某些元素调用构造函数?
- 在共享缓冲区内存中创建 ::std::string 对象
- 内存未释放 std::list<std::shared_ptr<std::string>> C++
- <char> 使用 Vulkan 映射内存时如何使用 std::vector 而不是 void**?
- 我可以将新的 std::tuple 放入内存映射区域,并在以后读回吗?
- 如何防止使用 std::shared_ptr 的代码中的内存泄漏
- C++ 中 std::vector 的内存问题
- std::set 是否将对象连续存储在内存中?
- 如何在窗口之间移动 std::unique_ptr 而不会冒内存泄漏的风险?
- 为什么"std::uninitialized_copy"通常取消对未初始化内存的迭代器的引用不是未定
- vector是否为std::移动的对象连续分配内存
- Valgrind 在 std::make_unique 中显示内存泄漏
- std::initializer_list 堆是否分配内存?
- std::unordered_set 中的元素如何存储在C++内存中?