【问题标题】:questions about memory pool关于内存池的问题
【发布时间】:2011-10-08 13:03:19
【问题描述】:

我需要对内存池的概念和实现进行一些说明。

维基上的memory pool,上面写着

也称为固定大小块分配,..., 因为这些实现由于变量而遭受碎片化 块大小,不可能在实时系统中使用它们 由于性能。

“可变块大小导致碎片”是如何发生的?固定大小的分配如何解决这个问题?这个 wiki 描述对我来说听起来有点误导。我认为固定大小的分配或可变大小无法避免碎片。在内存池上下文中,通过为特定应用程序设计的特定内存分配器来避免碎片,或者通过限制使用预期的内存块来减少碎片。

同样通过几个实现示例,例如Code Sample 1Code Sample 2,在我看来,要使用内存池,开发人员必须非常了解数据类型,然后将数据切割、拆分或组织成链接的内存块(如果数据接近链表)或分层链接块(如果数据更分层组织,如文件)。此外,似乎开发人员必须事先预测他需要多少内存。

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

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

感谢任何cmets!

【问题讨论】:

  • 我认为基本思想是你为每一类对象都有一个池,这样每个对象都需要相同的空间。这样,您可以轻松地分配、释放和重用内存,因为您可以精确地重用释放的块。
  • 如果内存池实现是由它允许分配的项目类型模板化的,那么编译器会隐藏它繁琐的事实,生成大量必要的样板代码。
  • Modern C++ Design 对这个话题进行了很好的讨论,我推荐这本书在话题之外也很有趣。
  • C++ 默认使用 C 分配器,它偏向于更大的分配。据我了解,分配 4 个字节与 64 个字节一样多。如果您的应用程序在小分配上非常繁重(智能指针是书中的示例),您可以看到通过摊销使用 C 分配器的性能成本获得的性能提升。
  • 在任何情况下,你可能不会从为你的类配备池分配器中获得太多好处(并且可能会受到影响)。您真正了解您的场景并进行比较非常重要。默认分配器已经非常聪明和高效,滚动自己的优点是非常情境。也请尝试一些现有的分配器,例如 tcmallocnedmalloc

标签: c++ memory-management fragmentation memory-pool


【解决方案1】:

可变块大小确实会导致碎片。看我附上的图:

图像 (from here) 显示了 A、B 和 C 分配内存块、大小可变的块的情况。

在某个时刻,B 释放了它所有的内存块,突然你有了碎片。例如,如果 C 需要分配一大块内存,这仍然适合可用内存,但它不能这样做,因为可用内存被分成两个块。

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

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

至于 C++ 中哪个内存池好,我会说这取决于。我在 VxWorks 下使用过一个操作系统提供的;从某种意义上说,一个好的内存池与操作系统紧密集成是有效的。实际上,我猜每个 RTOS 都提供了内存池的实现。

如果您正在寻找通用内存池实现,请查看this

编辑:

从您上次的评论来看,在我看来,您可能将内存池视为碎片问题的“解决方案”。不幸的是,这种情况并非如此。如果你愿意,碎片化是熵在内存层面的表现,即它是不可避免的。另一方面,内存池是一种管理内存的方式,可以有效地减少碎片的影响(正如我所说,正如维基百科所提到的,主要是在特定系统上,如实时系统)。这是有代价的,因为内存池的效率可能低于“正常”内存分配技术,因为您有最小的块大小。换句话说,熵在伪装下重新出现。

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

内存管理确实是一件复杂的事情,而内存池只是一种技术,与其他技术一样,与其他技术相比,它改进了一些东西,并产生了自己的成本。

【讨论】:

  • 感谢 sergio,“如果您考虑每个内存块大小相同的情况,显然不会出现这种情况。”除非您只有一种数据类型,否则每个相同大小的块怎么可能?
  • 嗨,塞尔吉奥,我读了图片链接。不幸的是,这是一个关于外部碎片的典型示例介绍,就像 Martinho Fernandesand 在他的帖子中解释的那样。我认为这并不能澄清我的问题。例如,它们也可以具有相同的大小但也不连续,并且 c 仍然无法请求内存。
【解决方案2】:

在您总是分配固定大小的块的情况下,您要么有足够的空间再分配一个块,要么没有。如果有,则该块适合可用空间,因为所有可用或已用空间的大小相同。碎片化不是问题。

在具有可变大小块的场景中,您最终可能会得到多个大小不同的独立空闲块。对大小小于可用总内存的块的请求可能无法满足,因为没有一个足够大的连续块来满足它。例如,假设您最终有两个独立的 2KB 空闲块,并且需要满足对 3KB 的请求。即使有足够的可用内存,这些块都不足以提供这一点。

【讨论】:

  • 谢谢,马蒂尼奥。如果“需要什么数据大小,创建什么池”是真的,那么我可以理解内存池。当然,首选一个特定的池,而不是一个通用的池”。无论有多少个池,内存使用都受到严格控制。
  • 是的,固定大小块的内存池通常特定于某些数据类型。您可以使用模板编写通用实现,但每个池只有相同类型的对象,因此是固定大小的。
【解决方案3】:

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

对于可变大小,这可能会导致问题,因为可能没有足够大的空闲块来满足特定请求的大小。

另一方面,对于固定大小的池,这不是问题,因为只能请求预定义大小的一部分。如果有空闲空间,则保证足够大以容纳一部分(的倍数)。

【讨论】:

    【解决方案4】:

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

    我曾经在一个军事系统上工作,我们必须计算系统可能使用的每种大小的内存块的最大可能数量。然后将这些数字加到总计中,并为系统配置了该数量的内存。

    非常昂贵,但为防守工作。


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

    【讨论】:

    • 这也是我想问的一个问题,当我学会了“一池一池”之后,会发生什么?
    【解决方案5】:

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

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

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

    很容易实现;我们在我们的应用程序中使用这种策略。这会在程序开始时导致大量内存分配,但不会再发生内存释放/分配,这会导致大量开销。

    【讨论】:

    • 嗨,James,如果“内存池总是为相同大小的单个数据类型完成”,那么我们应该为内存池选择哪种数据类型,什么不呢?
    • 这取决于您尝试存储的数据类型。在我们的示例中,我们正在处理所有相同尺寸的图像。所以它是一个代表图像像素的二维数组/矩阵。全局变量是一个指针数组。
    猜你喜欢
    • 2016-03-14
    • 2013-05-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-11
    • 2013-03-07
    • 2011-05-14
    相关资源
    最近更新 更多