内存池相关问题

8
我需要澄清关于内存池的概念和实现方面的问题。根据维基百科上的 内存池,它说:
所谓固定大小块分配 ... ,由于变量块大小导致的碎片化,这些实现可能无法在实时系统中使用,因为会影响性能。
“变量块大小如何导致碎片化”是如何发生的呢?如何通过固定大小的分配来解决这个问题?对我来说,这篇维基百科描述有点误导。我认为碎片化不是通过固定大小的分配避免或由变量大小引起的。在内存池上下文中,通过专门设计用于特定应用程序的内存分配器来避免碎片化,或者通过严格使用预期的内存块来减少碎片化。
此外,通过几个实现示例,例如代码示例1代码示例2,在我看来,要使用内存池,开发人员必须非常了解数据类型,然后将数据剪切、分割或组织成链接的内存块(如果数据接近于链接列表)或分层链接块(如果数据更具分层结构,如文件)。此外,似乎开发人员必须预测需要多少内存。

嗯,我可以想象这对于原始数据数组很有效。那么对于C++非原始数据类,其中内存模型并不那么明显呢?即使对于原始数据,开发人员是否应该考虑数据类型对齐?

有没有适用于C和C ++的良好内存池库?

感谢任何评论!


4
基本思想是为每个对象类别创建一个内存池,以便每个对象需要相同的空间。这样,您可以轻松地分配、释放和重用内存,因为您可以精确地重用已释放的块。 - Kerrek SB
1
如果一个内存池的实现是通过允许分配的项目类型进行模板化的,那么它繁琐的事实会被编译器生成大部分必要的样板代码所隐藏。 - Chad
1
《现代 C++ 设计》对这个主题有很好的讨论,我也推荐这本书不仅在这个主题方面有趣。 - Tom Kerr
1
C++默认使用C分配器,它偏向于较大的分配。据我所知,分配4个字节和64个字节的代价几乎相同。如果您的应用程序在小型分配方面非常重(智能指针是书中的例子),则可以通过分摊使用C分配器的性能成本来获得性能提升。 - Tom Kerr
1
无论如何,为你的类配备池分配器可能不会带来太多好处(甚至可能会受到影响)。重要的是你需要对你的场景进行性能分析和比较。默认的分配器已经相当聪明和高效了,自己编写的优势非常局限。同时也可以尝试一些现有的分配器,例如tcmallocnedmalloc - Kerrek SB
显示剩余4条评论
5个回答

14

变量块大小确实会导致内存碎片。看一下我附加的图片:enter image description here

这张图片(来自此处)展示了一种情况,即A、B和C分配了可变大小的内存块。

在某个时刻,B释放了其所有的内存块,于是你就有了内存碎片。例如,如果C需要分配一个仍然可以适应可用内存的大块内存,它无法这样做,因为可用内存被分成了两块。

现在,如果您考虑每个内存块都是相同大小的情况,这种情况显然不会出现。

当然,内存池也有自己的缺点,正如你自己所指出的那样。所以你不应该认为内存池是一种神奇的魔杖。它有一个代价,在特定的情况下支付它是有意义的(即具有有限内存、实时约束等的嵌入式系统)。

至于哪个C++内存池比较好,我想说这要取决于具体情况。我在VxWorks下使用过一个由操作系统提供的内存池;在某种程度上,当内存池与操作系统紧密集成时,它才是有效的。实际上,每个RTOS都提供了内存池的实现,我想。

如果你正在寻找通用的内存池实现,请查看这里

编辑:

从你上一条评论来看,似乎你认为内存池可能是解决碎片问题的“唯一”方法。很遗憾,这并不正确。碎片化是内存层面熵的表现,也就是说,它是不可避免的。另一方面,内存池是一种管理内存的方式,可以有效地减少碎片化的影响(正如我所说的,以及维基百科提到的那样,主要适用于特定的系统,如实时系统)。但这也会有代价,因为内存池可能比“普通”的内存分配技术效率低,因为你需要一个最小块大小。换句话说,熵会在伪装下再次出现。

此外,许多参数影响内存池系统的效率,例如块大小、块分配策略,或者您只有一个内存池还是有几个具有不同块大小、不同生命周期或不同策略的内存池。

内存管理确实是一个复杂的问题,而内存池只是一种技术,可以在与其他技术相比改善事物并产生其自身的代价。


谢谢Sergio,“如果你考虑每个内存块的大小相同的情况,这种情况显然不会出现。”除非只有单一的数据类型,否则如何可能每个块的大小都相同? - pepero
你好,Sergio,我看了图片链接。不幸的是,那只是一个关于外部碎片的典型示例介绍,就像Martinho Fernandes在他的帖子中解释的那样。我认为这并没有澄清我的问题。例如,它们也可以具有相同的大小但不是连续的,而C仍然无法请求内存。 - pepero

5
在分配固定大小块的情况下,你要么有足够的空间来分配一个新块,要么没有。如果有足够的空间,那么该块将适合可用空间,因为所有的空闲或已使用的空间大小都相同。这时碎片化不是问题。
在变量大小块的场景中,你可能会得到多个不同大小的独立空闲块。如果请求的块大小小于总空闲内存大小,则可能无法满足请求,因为没有足够大的连续块可用。例如,假设你有两个2KB的独立空闲块,并需要满足一个3KB的请求。即使有足够的内存可用,这两个块也都不足以提供所需的空间。

谢谢,Martinho。如果“需要什么数据大小,创建什么池”是正确的,那么我就能理解内存池了。当然,最好使用具体的池,而不是一个通用的池来处理所有事情。无论有多少池,内存使用都受到严格控制。 - pepero
是的,固定大小块的内存池通常针对特定的数据类型。您可以使用模板编写通用实现,但每个内存池仅包含相同类型的对象,因此具有固定大小。 - R. Martinho Fernandes

2

固定大小和可变大小的内存池都会出现碎片化,即在已使用的内存块之间会有一些空闲的内存块。

对于可变大小的内存池,这可能会引起问题,因为可能没有足够大的空闲块来满足某个特定请求的大小。

另一方面,对于固定大小的内存池,这不是一个问题,因为只能请求预定义大小的部分。如果有可用空间,则保证足够大(是一个或多个部分的倍数)。


1

如果你在做一个硬实时系统,你可能需要提前知道你可以在允许的最大时间内分配内存。这可以通过固定大小的内存池来“解决”。

我曾经在一个军事系统上工作过,在那里我们不得不计算系统可能使用的每种大小的内存块的最大可能数量。然后将这些数字加起来,用这个数量的内存配置系统。

非常昂贵,但对国防有用。


当您有多个固定大小的池时,可能会出现二次碎片,即使其他池中有足够的空间,您的池也可能用完了块。您如何共享它?


这也是我想问的问题,当我学会了“一种类型一个池”的概念后,会发生什么? - pepero

1

使用内存池,操作可能如下:

  1. 存储一个全局变量,它是可用对象的列表(最初为空)。
  2. 要获取新对象,请尝试从全局可用对象列表中返回一个。 如果没有,则调用 operator new 在堆上分配新对象。 分配非常快速,这对于某些当前可能花费大量 CPU 时间进行内存分配的应用程序非常重要。
  3. 要释放对象,只需将其添加到全局可用对象列表即可。 您可以对全局列表中允许的项目数设置上限; 如果达到上限,则该对象将被释放而不是返回到列表中。 上限可防止出现大规模内存泄漏。

请注意,这始终针对相同大小的单个数据类型执行; 它不适用于较大的数据类型,然后您可能需要像通常一样使用堆。

它非常容易实现; 我们在我们的应用程序中使用此策略。 这会在程序开始时引起一堆内存分配,但不再发生更多的内存释放/分配,这会产生显着的开销。


嗨,詹姆斯,如果“内存池总是针对相同大小的单一数据类型进行操作”,那么我们应该选择什么样的数据类型作为内存池呢?又有哪些不适合呢? - pepero
这取决于您要存储的数据类型。在我们的示例中,我们正在处理尺寸相同的所有图像。因此,它是表示图像像素的二维数组/矩阵。全局变量是指针数组。 - James Johnston

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