6

我注意到 Linux 中关于内存使用 (RES) 报告的一些有趣行为top。我附上了以下程序,它在堆上分配了几百万个对象,每个对象都有一个大约 1 KB 的缓冲区。指向这些对象的指针由 astd::list或 a跟踪std::vector。我注意到的有趣行为是,如果我使用 a std::list,则报告的内存使用情况top在睡眠期间永远不会改变。但是,如果我使用std::vector,则在这些睡眠期间内存使用量将降至接近 0。

我的测试配置是:
Fedora Core 16
Kernel 3.6.7-4
g++ version 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 将 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);
}
4

4 回答 4

3

内存的释放是在“块”的基础上完成的。当您使用列表时,很有可能内存会被分割成小块。

当您使用向量进行分配时,所有元素都存储在一个大块中,因此内存释放代码很容易说“天哪,我这里有一个非常大的空闲区域,我将把它释放回操作系统”。也完全有可能在增大向量时,内存分配器进入“大块模式”,它使用与“小块模式”不同的分配方法 - 例如,您分配超过 1MB,内存分配代码可能会看到作为开始使用不同策略的好时机,只需向操作系统询问“完美契合”的内存。这个大块在被释放时很容易释放回操作系统。

另一方面,如果您要添加到列表中,您会不断地要求小块,因此分配器使用不同的策略,要求大块然后分配小块。确保块中的所有块都已被释放既困难又耗时,因此分配器很可能“不打扰”——因为很可能那里有一些区域“仍在使用”,然后它就可以了无论如何都不能被释放。

我还要补充一点,使用“top”作为内存度量并不是一种特别准确的方法,而且非常不可靠,因为它在很大程度上取决于操作系统和运行时库的功能。属于一个进程的内存可能不是“常驻”的,但该进程仍然没有释放它——它只是不“存在于实际内存中”(而是在交换分区中!)

对于您的问题“这是在某处定义的”,我认为在某种意义上是 C/C++ 库源代码定义了它。但它并没有被定义为某处写着“这就是它的工作方式,我们承诺永远不会挂掉它”。作为 glibc 和 libstdc++ 提供的库不会这么说,它们会随着新技术和想法的发明而改变 malloc、free、new 和 delete 的内部结构——有些可能会使事情变得更好,有些可能会使事情变得更糟,对于给定的设想。

正如评论中指出的那样,内存没有锁定到进程。如果内核认为内存更好地用于其他用途[并且内核在这里是无所不能的],那么它将从一个正在运行的进程中“窃取”内存并将其提供给另一个进程。尤其是很久没有“触动”过的记忆。

于 2012-12-31T22:43:30.310 回答
2

1. std::vector 和 std::list 都在使用 std::allocator。即使 std::vector 正在执行批量重新分配,std::allocator 不会以几乎相同的安排将内存分配给 std::list 和 std::vector 吗?这个程序毕竟是单线程的。

那么,有什么区别?

  • std::list一个接一个地分配节点(每个节点除了你的 之外还需要两个指针Foo *)。此外,它永远不会重新分配这些节点(这是由 的迭代器无效要求保证的list)。因此,std::allocator它将从底层机制请求一系列固定大小的块(可能malloc又会使用sbrkormmap系统调用)。这些固定大小的块可能比列表节点大,但如果是这样,它们都将与std::allocator.

  • std::vector分配一个连续的指针块,没有记账开销(全部在向量父对象中)。每次 apush_back溢出当前分配时,向量将分配一个新的更大的块,将所有内容移至新块,并释放旧块。现在,新块将是旧块大小的两倍(或 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 方案,以及它与任何系统调用 (3) 最终使用的交互

在您的特定情况下,我可以想到一个可能的解释,即向量显然比列表释放更多内存。

考虑向量以单个连续分配结束,并且许多Foos 也将连续分配。这意味着当您释放所有这些内存时,很容易发现大多数底层页面都是真正免费的。

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

于 2013-01-01T00:03:35.893 回答
1

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

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

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

于 2014-03-13T15:22:29.240 回答
0

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

还有 ptmalloc 源代码。

于 2013-01-01T00:54:17.600 回答