当使用std::vector和std::list时,Linux内存使用排在最前面

Linux Memory Usage in top when using std::vector versus std::list

本文关键字:std 内存 前面 Linux list vector      更新时间:2023-10-16

我注意到Linux中关于top报告的内存使用(RES)的一些有趣行为。我附上了下面的程序,它在堆上分配了几百万个对象,每个对象都有一个大约1kb的缓冲区。指向这些对象的指针由std::liststd::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,它将反过来使用sbrkmmap系统调用)。这些固定大小的块可能比列表节点大,但如果是这样,它们都将是std::allocator使用的默认块大小。

  • std::vector分配一个连续的指针块,没有记账开销(这都在vector父对象中)。每次push_back溢出当前分配时,vector将分配一个新的更大的块,将所有内容移动到新块中,并释放旧块。现在,新块的大小将是旧块的两倍(或1.6倍,或其他),因为需要保持push_back平摊常数时间保证。所以,很快,我希望它请求的大小超过std::allocator的任何明智的默认块大小。

所以,有趣的交互是不同的:一个在std::vector和分配器的底层机制之间,一个在std::allocator本身和底层机制之间。

2。我在哪里可以了解Linux的内存分配行为。我听说过Linux将RAM分配给进程的说法即使在它释放了它之后,但我不知道这种行为是否保证。为什么使用std::vector会对这种行为产生如此大的影响?

有几个级别你可能会关心:

  1. 容器自己的分配模式:希望上面描述过
      请注意,在实际应用中,容器的使用方式同样重要
  2. std::allocator本身,它可以为小的分配提供一层缓冲
    • 我认为这不是标准所要求的,所以这是具体到你的实施
  3. 底层分配器,这取决于您的std::allocator实现(例如,它可以是malloc,但它是由您的libc实现的)
  4. 内核使用的VM方案,以及它与最终使用
  5. 的任何系统调用(3)的交互

在你的特殊情况下,我可以想到一个可能的解释,为什么向量明显比列表释放更多的内存。

考虑该向量最终以单个连续分配结束,并且许多Foo s也将连续分配。这意味着,当您释放所有这些内存时,很容易发现大多数底层页面确实是空闲的。

现在考虑列表节点分配与Foo实例1:1交错。即使分配器做了一些批处理,堆看起来也可能比std::vector的情况更加碎片化。因此,当您释放已分配的记录时,需要做一些工作来确定底层页面现在是否空闲,并且没有特别的理由期望会发生这种情况(除非随后的大分配鼓励堆记录合并)。

答案是malloc "fastbins"优化。list创建了很小的(小于64字节)分配,当它释放它们时,它们实际上并没有被释放——而是进入了快速块池。这种行为意味着即使在列表被清除后堆仍然是碎片化的,因此它不会返回系统。

您可以使用malloc_trim(128*1024)来强制清除它们。或者使用mallopt(M_MXFAST, 0)来完全禁用fastbin。

我发现第一个解决方案是更正确的,如果你调用它时,你真的不需要内存了。

较小的块通过brk和调整数据段并不断分裂和融合,而较大的块mmap的过程较少受到干扰。更多信息(PDF)