当内存池不是一个好主意时避免内存碎片

Max*_*hon 5 c++ memory memory-management fragmentation dynamic-memory-allocation

我正在开发一个 C++ 应用程序,该程序会无限地运行,随着时间的推移分配和释放数百万个字符串 (char*)。RAM的使用是程序中需要认真考虑的因素。这会导致 RAM 使用率随着时间的推移越来越高。我认为问题在于堆碎片。我真的需要找到一个解决方案。

内存使用报告 1

您可以在图中看到,在程序中进行数百万次分配和释放之后,使用量只是在增加。从我测试它的方式来看,我知道它存储的数据并没有增加。我猜你会问,“你怎么确定这一点?”,“你怎么确定这不仅仅是内存泄漏?”,好吧。

在此输入图像描述

这个测试运行的时间要长得多。只要有可能,我就会malloc_trim(0)在我的程序中运行。看来,应用程序最终可以将未使用的内存返回给操作系统,并且它几乎为零(我的程序当前拥有的实际数据大小)。这意味着问题不是内存泄漏。但我不能依赖这种行为,我的程序的分配和释放模式是随机的,如果它从不释放内存怎么办?

  • 我在标题中说过内存池对于这个项目来说是一个坏主意。当然我没有绝对的知识。但我分配的字符串可以是 30-4000 字节之间的任意值。这使得许多优化和聪明的想法变得更加困难。内存池就是其中之一。
  • 我用作GCC 11 / G++ 11编译器。如果某些旧版本的分配器不好。我不应该有这个问题。
  • 我如何获取内存使用情况?Pythonpsutil模块。proc.memory_full_info()[0],这给了我RSS.
  • 当然,你不知道我的程序的细节。如果这确实是由于堆碎片造成的,那么这仍然是一个有效的问题。我可以说的是,我正在保留有关发生了多少分配和释放的最新信息。我知道程序中每个容器的元素计数。但如果您对问题的原因仍然有一些想法,我愿意接受建议。
  • 我不能只是为所有字符串分配 4096 字节,这样优化起来会更容易。这正是我想做的相反的事情。

所以我的问题是,在一个随着时间的推移发生数百万次分配和释放的应用程序中,程序员应该做什么(我应该做什么),并且它们的大小不同,因此内存池很难有效使用。我无法改变程序的作用,我只能改变实现细节。

赏金编辑:当尝试使用内存池时,是否可以创建多个内存池,以便每个可能的字节数都有一个池?例如,我的字符串可能在 30-4000 字节之间。因此,有人不能4000 - 30 + 1为程序的每个可能的分配大小创建 3971 个内存池吗?这不适用吗?所有池都可以从小规模开始(不会损失太多内存),然后扩大,以在性能和内存之间取得平衡。我并不是想利用内存池预先保留大空间的能力。由于频繁的分配和释放,我只是想有效地重用释放的空间。

最后编辑:事实证明,图表中出现的内存增长实际上来自我程序中的 http 请求队列。我没有看到我所做的数十万次测试使这个队列变得臃肿(类似于 webhook)。图 2 的合理解释是,我最终从服务器禁止了 DDOS(或者由于某种原因无法再打开连接),队列清空,RAM 问题解决。因此,以后阅读这个问题的任何人,请考虑每种可能性。我从来没想过会发生这样的事情。不是内存泄漏,而是实现细节。我仍然认为@Hajo Kirchhoff 值得赏金,他的回答确实很有启发性。

Haj*_*off 1

如果一切确实如您所说,并且没有您尚未发现的错误,那么请尝试以下操作:

无论如何,malloc 和其他内存分配通常使用 16 字节的块,即使实际请求的大小小于 16 字节。所以你只需要 4000/16 - 30/16 ~ 250 个不同的内存池。

const int chunk_size = 16;

memory_pools pool[250]; // 250 memory pools, managing '(idx+1)*chunk_size' size

char* reserve_mem(size_t sz)
{
    size_t pool_idx_to_use = sz/chunk_size;
    char * rc=pool[pool_idx_to_use].allocate();
}
Run Code Online (Sandbox Code Playgroud)

IOW,你有 250 个内存池。pool[0]分配并管理长度为16字节的chunk。pool[100] 管理 1600 字节等的块...

如果您提前知道字符串的长度分布,则可以根据此知识为池保留初始内存。否则我可能会以 4096 字节的增量为池保留内存。

因为虽然 malloc C 堆通常以 16 字节的倍数分配内存,但它会(至少在 Windows 下,但我猜测 Linux 也类似)向操作系统请求内存 - 通常适用于 4K 页。IOW,由操作系统管理的“外部”内存堆保留并释放 4096 字节。

因此,将您自己的内部内存池增加到 4096 字节意味着操作系统应用程序堆中不会出现碎片。这个 4096 页大小(或倍数...)来自处理器架构。Intel 处理器的内置页面大小为 4K(或其倍数)。不知道其他处理器,但我怀疑那里有类似的架构。

所以,总结一下:

每个内存池的字符串使用 16 字节倍数的块。使用 4K 字节倍数的块来增加内存池。

这将使应用程序的内存使用与操作系统的内存管理保持一致,并尽可能避免碎片。

从操作系统的角度来看,您的应用程序只会以 4K 块的形式增加内存。这非常容易分配和释放。而且不存在碎片。

从内部 (lib) C 堆管理的角度来看,您的应用程序将使用内存池,并且每个字符串最多浪费 15 个字节。此外,所有类似的长度分配都将堆在一起,因此也不会产生碎片。