巨大的std::vector<std::vector>在销毁时没有释放所有内存

24

当使用非常大的向量向量时,我们发现部分内存没有被释放。

#include <iostream>
#include <vector>
#include <unistd.h>

void foo()
{
    std::vector<std::vector<unsigned int> > voxelToPixel;
    unsigned int numElem = 1<<27;
    voxelToPixel.resize( numElem );

    for (unsigned int idx=0; idx < numElem; idx++)
        voxelToPixel.at(idx).push_back(idx);

}

int main()
{
    foo();
    std::cout << "End" << std::endl;
    sleep(30);
    return 0;
}

这意味着大约有4GB的内存会一直被占用,直到进程结束。

如果我们将for行更改为

for (unsigned int idx=0; idx < numElem; idx++)
    voxelToPixel.at(0).push_back(idx);

内存已释放。

在Linux机器上使用gcc-4.8。我们使用htop来跟踪计算机上的内存使用情况,计算机配有100 GB的RAM。您需要约8 GB的RAM才能运行代码。您是否能复现问题?关于为什么会发生这种情况,您有任何想法吗?

编辑: 我们发现在Mac(使用gcc或clang)中不会出现这种情况。此外,在Linux中,如果我们调用foo两次,内存就会被释放(但第三次再次发生)。


1
你能用一个更小的例子来重现这个问题吗? - stefan
@stefan 更小的话,是指元素数量更少吗?我曾经在2^25上看到过这种情况,但对于更小的情况很难确定。 - quimnuss
1个回答

29

小额分配(默认为128kb以下)由进程内存池管理,回收时不返回给操作系统而是用于进程内存池的重复利用。大额分配直接从操作系统获取(通过调用 mmap 函数),回收时会返回给操作系统。

在您的第一个示例中,每个向量只需要为单个 int 分配足够的空间。您有一亿个小额分配,这些分配都不会被返回到操作系统。

在第二个示例中,随着向量的增长,它将进行许多不同大小的分配。其中一些小于 mmap 阈值,这些将保留在进程内存中;但是,由于您只对一个向量执行此操作,因此这不会占用太多内存。如果您使用 resizereserve 为每个向量分配所有内存,然后再填充它们,那么您应该发现所有内存都会被返回给操作系统。


每个 1<<27 内部向量只包含一个 int,因此在这种情况下 resize/reserve 无法帮助。外部向量已经有了 resize - Mark Ransom
@MarkRansom:哎呀,你说得对,我读错了代码。 - Mike Seymour
1
@quimnuss:你可以使用自定义分配器来分配一个巨大的内存块,将其分配给向量,并在使用后释放所有内存;Boost.Pool可能会有所帮助。你可以尝试使用malloc_trim - 但它只能释放堆的第一个“已使用”部分之前的内存。一旦堆被分段,其中散布着已使用内存块,你就没有太多可做的了。如果操作系统耗尽了内存,那么它就会释放可怕的oom-killer,生活变得真正可怕。 - Mike Seymour
2
@quimnuss 没有必要强制释放未使用的堆内存。操作系统已经可以回收物理内存,而虚拟内存成本低廉。 - David Schwartz
2
在Linux中,内存没有被仔细地分配。您可以将数据写入允许用于堆的虚拟内存区域,然后Linux会找到一些RAM来支持它。您可以通过使用“brk”系统调用设置数据段的大小来告诉Linux您已经完成了对整个内存区域的使用。C++分配器将为您处理此操作。如果您不释放内存,但也不一段时间内不使用它,Linux将将其分页到磁盘上,并让其他进程或磁盘缓存使用RAM。 - Stuart Caie
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接