【问题标题】:What is memory fragmentation?什么是内存碎片?
【发布时间】:2011-04-15 18:55:39
【问题描述】:

我听说过在 C++ 动态内存分配的上下文中多次使用“内存碎片”这个术语。我发现了一些关于如何处理内存碎片的问题,但找不到处理它本身的直接问题。所以:

  • 什么是内存碎片?
  • 如何判断内存碎片是否是我的应用程序的问题?哪种程序最有可能受到影响?
  • 处理内存碎片的常用方法有哪些?

还有:

  • 我听说大量使用动态分配会增加内存碎片。这是真的?在 C++ 的上下文中,我了解所有标准容器(std::string、std::vector 等)都使用动态内存分配。如果在整个程序中使用这些(尤其是 std::string),内存碎片是否更可能成为问题?
  • 如何在 STL 密集型应用程序中处理内存碎片?

【问题讨论】:

标签: c++ memory heap-memory fragmentation


【解决方案1】:

假设你有一个“大”(32 字节)的可用内存:

----------------------------------
|                                |
----------------------------------

现在,分配一些(5 次分配):

----------------------------------
|aaaabbccccccddeeee              |
----------------------------------

现在,释放前四个分配,而不是第五个:

----------------------------------
|              eeee              |
----------------------------------

现在,尝试分配 16 个字节。哎呀,我做不到,尽管有将近两倍的免费空间。

在具有虚拟内存的系统上,碎片问题比您想象的要少,因为大型分配只需要在虚拟地址空间中是连续的,而不是在物理中地址空间。所以在我的例子中,如果我有一个页面大小为 2 个字节的虚拟内存,那么我可以毫无问题地分配我的 16 个字节。物理内存如下所示:

----------------------------------
|ffffffffffffffeeeeff            |
----------------------------------

而虚拟内存(更大)可能如下所示:

------------------------------------------------------...
|              eeeeffffffffffffffff                   
------------------------------------------------------...

内存碎片的典型症状是您尝试分配一个大块但您不能,即使您似乎有足够的可用内存。另一个可能的后果是进程无法将内存释放回操作系统(因为它从操作系统分配的每个大块,用于malloc 等进行细分,都会留下一些东西,即使大多数每个块现在都没有使用)。

在 C++ 中防止内存碎片的策略是根据对象的大小和/或预期的生命周期从不同的区域分配对象。因此,如果您要创建大量对象并稍后将它们全部销毁,请从内存池中分配它们。您在它们之间进行的任何其他分配都不会来自池,因此不会位于它们之间的内存中,因此内存不会因此而碎片化。或者,如果您要分配大量相同大小的对象,则从同一个池中分配它们。然后,池中的一段可用空间永远不会小于您尝试从该池分配的大小。

一般来说你不需要太担心它,除非你的程序是长时间运行的并且做了很多分配和释放。当您同时拥有短寿命和长寿命对象时,您的风险最大,但即便如此,malloc 也会尽最大努力提供帮助。基本上,忽略它,直到您的程序分配失败或意外导致系统内存不足(在测试中发现这一点,优先考虑!)。

标准库并不比其他分配内存的库差,标准容器都有一个Alloc模板参数,如果绝对必要,您可以使用它来微调它们的分配策略。

【讨论】:

  • 那么每个字符都是一个字节?这将使您的“大范围”== 32 字节(我猜- 没算):) 很好的例子,但在最后一行之前提到单位会有所帮助。 :)
  • @jalf:是的。我根本不打算提到单位,然后意识到我不得不提到。在您发表评论时正在处理它。
  • 很难选择一个“答案”——这里有很多很棒的答案,我鼓励任何有兴趣的人阅读所有这些。不过,我认为你在这里涵盖了所有重要的观点。
  • “标准库并不比其他任何分配内存的库差”。如果是真的,那就太好了,但是标准 C++ 模板(如字符串和向量)的实现在调整大小时可能会出现一些非常不受欢迎的行为。例如,在旧版本的 Visual Studio 中,std::string 基本上按 realloc 1.5 * current_size 调整大小(最接近的 8 个字节)。因此,如果您继续附加到一个字符串,您可以很容易地取消堆,尤其是在嵌入式系统上。最好的防御措施是保留您预期使用的空间量,以避免隐藏的重新分配。
  • @du369:虚拟内存没有像物理内存那样严重碎片化。 ffffffffffffffff 是虚拟内存中的连续分配,但物理内存中不可能存在这样的连续分配。如果您更喜欢看它们同样分散,但虚拟空间要大得多,那么请随意以这种方式看待它。重要的实用点是,使用巨大的虚拟地址空间通常足以能够忽略碎片,所以只要它允许我分配 16 字节,它就会有所帮助。
【解决方案2】:

什么是内存碎片?

内存碎片是指您的大部分内存被分配到大量不连续的块或块中 - 使您的总内存中有很大一部分未分配,但在大多数典型情况下无法使用。这会导致内存不足异常或分配错误(即 malloc 返回 null)。

考虑这个问题的最简单方法是想象你有一堵空的大墙,你需要在上面放置不同尺寸的图片。每张图片都占据一定的尺寸,您显然无法将其拆分成更小的部分以使其适合。你需要在墙上有一个空的地方,图片的大小,否则你不能把它放上去。现在,如果你开始把照片挂在墙上,而你不小心如何安排它们,你很快就会得到一堵部分被照片覆盖的墙,即使你可能有空白点,但大多数新照片都不适合因为它们比可用的位置大。您仍然可以挂非常小的图片,但大多数图片不适合。所以你必须重新排列(紧凑)已经在墙上的那些,以便为更多空间腾出空间。

现在,想象墙是你的(堆)内存,而图片是对象。这就是内存碎片。

如何判断内存碎片是否是我的应用程序的问题?哪种程序最有可能受到影响?

您可能正在处理内存碎片的一个明显迹象是,如果您遇到许多分配错误,尤其是当已用内存的百分比很高时 - 但不是您还没有用完所有内存 - 所以从技术上讲,您应该有您尝试分配的对象有足够的空间。

当内存严重碎片化时,内存分配可能会花费更长的时间,因为内存分配器必须做更多的工作才能为新对象找到合适的空间。如果反过来你有很多内存分配(你可能会这样做,因为你最终会出现内存碎片)分配时间甚至可能会导致明显的延迟。

处理内存碎片的常用方法有哪些?

使用好的算法来分配内存。不要为许多小对象分配内存,而是为这些较小对象的连续数组预分配内存。有时在分配内存时有点浪费可能会提高性能,并且可以省去处理内存碎片的麻烦。

【讨论】:

  • +1。我刚刚删除了我提出的答案,因为您的“墙上的图片”比喻真的非常非常好,清晰。
  • 如果你强调图片必须有不同的尺寸,我会更喜欢它。否则不会产生碎片。
  • 有趣的是,main memory databases 现在变得有些实用(可用内存真的很大)。在这种情况下,值得注意的是,对于 HDD,从 RAM 中读取连续行要比碎片数据快得多。
  • 与墙上的图片进行了很好的视觉类比,但主内存不是二维的!不过,还是不错的答案,谢谢。
【解决方案3】:

内存碎片与磁盘碎片的概念相同:它指的是空间被浪费,因为正在使用的区域没有足够紧密地组合在一起。

假设你有十字节的内存:

 |   |   |   |   |   |   |   |   |   |   |
   0   1   2   3   4   5   6   7   8   9

现在让我们分配三个三字节块,名称为 A、B 和 C:

 | A | A | A | B | B | B | C | C | C |   |
   0   1   2   3   4   5   6   7   8   9

现在释放块 B:

 | A | A | A |   |   |   | C | C | C |   |
   0   1   2   3   4   5   6   7   8   9

现在如果我们尝试分配一个四字节的块 D 会发生什么?好吧,我们有四个字节的可用内存,但我们没有四个连续字节的可用内存,所以我们不能分配 D!这是对内存的低效使用,因为我们应该能够存储 D,但我们做不到。而且我们不能移动 C 来腾出空间,因为我们程序中的某些变量很可能指向 C,我们无法自动找到并更改所有这些值。

你怎么知道这是个问题?嗯,最大的迹象是您的程序的虚拟内存大小比您实际使用的内存量大得多。在实际示例中,您将拥有超过 10 个字节的内存,因此 D 只会从第 9 个字节开始分配,而第 3-5 个字节将保持未使用状态,除非您稍后分配了 3 个字节或更小的内容。

在这个例子中,3 个字节的浪费并不是很多,但考虑一个更病态的情况,即两个字节的两个分配,例如,在内存中相隔 10 兆字节,你需要分配一个块大小 10 兆字节 + 1 字节。你必须去向操作系统请求超过 10 兆字节的虚拟内存才能做到这一点,即使你已经拥有足够的空间只差一个字节。

如何预防?当您频繁地创建和销毁小对象时往往会出现最坏的情况,因为这往往会产生“瑞士奶酪”效果,许多小对象被许多小孔隔开,从而无法在这些孔中分配更大的对象。当你知道你将要这样做时,一个有效的策略是预先分配一大块内存作为你的小对象的池,然后手动管理该块中小对象的创建,而不是让默认分配器处理它。

通常,您执行的分配越少,内存碎片的可能性就越小。然而,STL 相当有效地处理了这个问题。如果您有一个字符串正在使用其当前分配的全部内容并且您将一个字符附加到它,它不会简单地重新分配其当前长度加一,它加倍它的长度。这是“频繁小分配池”策略的变体。该字符串正在占用一大块内存,以便它可以有效地处理大小重复的小幅增加,而无需进行重复的小幅重新分配。实际上所有的 STL 容器都在做这种事情,所以通常你不需要太担心由自动重新分配 STL 容器引起的碎片。

虽然 STL 容器当然不会在彼此之间共享内存,所以如果您要创建许多小容器(而不是几个经常调整大小的容器),您可能必须像对待任何频繁创建的小对象(无论是否为 STL)一样,关注防止碎片化。

【讨论】:

    【解决方案4】:
    • 什么是内存碎片?

    内存碎片是内存变得不可用的问题,即使它在理论上是可用的。有两种碎片:内部碎片是已分配但不能使用的内存(例如,当内存以 8 字节块分配但程序只需要 4 字节时重复执行单次分配)。 外部碎片是空闲内存被分成许多小块的问题,即使有足够的整体空闲内存,也无法满足大的分配请求。

    • 如何判断内存碎片是否是我的应用程序的问题?哪种程序最有可能受到影响?

    如果您的程序使用的系统内存比其实际有效数据所需的多得多(并且您已经排除了内存泄漏),那么内存碎片就是一个问题。

    • 处理内存碎片的常用方法有哪些?

    使用好的内存分配器。 IIRC,那些使用“最适合”策略的人通常在避免碎片化方面要好得多,如果慢一点的话。然而,也表明,对于任何分配策略,都存在病态的最坏情况。幸运的是,大多数应用程序的典型分配模式实际上对于分配器来说是相对良性的。如果您对细节感兴趣,那里有一堆论文:

    • Paul R. Wilson、Mark S. Johnstone、Michael Neely 和 David Boles。动态存储分配:调查和严格审查。在 1995 年的诉讼中 内存管理国际研讨会,Springer Verlag LNCS,1995 年
    • 马克·S.约翰斯通、保罗·R.威尔逊。内存碎片问题:解决了吗? 在 ACM SIG-PLAN Notices,第 34 卷第 3 期,第 26-36 页,1999 年
    • M.R. Garey、R.L. Graham 和 J.D. Ullman。内存分配算法的最坏情况分析。在第四届 ACM 计算理论年度研讨会上,1972 年

    【讨论】:

    • 不错。这些论文可以免费访问吗?
    • @rsonx:形式上没有(我认为),但是当我研究这个主题时(大约 20 年前),我能够在网上找到免费的副本。
    【解决方案5】:

    更新:
    Google TCMalloc: Thread-Caching Malloc
    发现在长时间运行的过程中处理碎片非常好。


    我一直在开发一个在 HP-UX 11.23/11.31 ia64 上存在内存碎片问题的服务器应用程序。

    看起来像这样。有一个进程进行内存分配和释放,并运行了好几天。即使没有内存泄漏,进程的内存消耗也在不断增加。

    关于我的经历。在 HP-UX 上,使用 HP-UX gdb 很容易找到内存碎片。你设置了一个断点,当你点击它时,你运行这个命令:info heap 并查看进程的所有内存分配和堆的总大小。然后你继续你的程序,然后一段时间后你再次遇到断点。你再做一次info heap。如果堆的总大小较大,但单独分配的数量和大小相同,则可能存在内存分配问题。如有必要,请先检查几次。

    我改善这种情况的方法是这样的。在我对 HP-UX gdb 进行了一些分析之后,我发现内存问题是由于我使用 std::vector 来存储数据库中的某些类型的信息这一事实引起的。 std::vector 要求其数据必须保存在一个块中。我有几个基于std::vector 的容器。这些容器会定期重新创建。经常会出现将新记录添加到数据库然后重新创建容器的情况。而且由于重新创建的容器更大,它们不适合可用的可用内存块,并且运行时要求操作系统提供一个更大的新块。结果,即使没有内存泄漏,进程的内存消耗也会增加。我在更换容器时改善了这种情况。我开始使用 std::deque 而不是 std::vector,它具有不同的数据分配内存方式。

    我知道在 HP-UX 上避免内存碎片的方法之一是使用 Small Block Allocator 或使用 MallocNextGen。在 RedHat Linux 上,默认分配器似乎可以很好地处理大量小块的分配。在 Windows 上有Low-fragmentation Heap,它解决了大量小分配的问题。

    我的理解是,在 STL 繁重的应用程序中,您必须首先确定问题。内存分配器(如在 libc 中)实际上处理了大量小分配的问题,这对于 std::string 来说很典型(例如,在我的服务器应用程序中,有很多 STL 字符串,但正如我从运行 info heap 中看到的那样,它们不是造成任何问题)。我的印象是您需要避免频繁的大分配。不幸的是,在某些情况下您无法避免它们并且必须更改您的代码。正如我所说的,我在切换到std::deque 时改善了这种情况。如果你确定了你的记忆碎片,也许可以更准确地谈论它。

    【讨论】:

      【解决方案6】:

      当您分配和释放许多不同大小的对象时,最有可能发生内存碎片。假设你在内存中有如下布局:

      obj1 (10kb) | obj2(20kb) | obj3(5kb) | unused space (100kb)
      

      现在,当obj2 被释放时,你有 120kb 的未使用内存,但你不能分配一个完整的 120kb 块,因为内存是碎片化的。

      避免这种影响的常用技术包括ring buffersobject pools。在 STL 的上下文中,std::vector::reserve() 之类的方法可以提供帮助。

      【讨论】:

        【解决方案7】:

        可以在这里找到关于内存碎片的非常详细的答案。

        http://library.softwareverify.com/memory-fragmentation-your-worst-nightmare/

        这是我 11 年来内存碎片化答案的巅峰之作

        【讨论】:

          【解决方案8】:

          什么是内存碎片?

          当您的应用使用动态内存时,它会分配和释放大块内存。一开始,您的应用程序的整个内存空间是一个连续的空闲内存块。但是,当您分配和释放不同大小的块时,内存开始变得碎片化,即不是一个大的连续空闲块和许多连续分配的块,而是一个已分配和空闲块混合。由于空闲块的大小有限,因此很难重用它们。例如。你可能有 1000 字节的空闲内存,但不能为 100 字节的块分配内存,因为所有空闲块的长度最多为 50 字节。

          另一个不可避免但问题较少的碎片来源是,在大多数架构中,内存地址必须对齐到 2、4、8 等字节边界(即地址必须是 2 的倍数) , 4, 8 等)这意味着即使你有例如一个包含 3 个 char 字段的结构,由于每个字段都与 4 字节边界对齐,因此您的结构的大小可能为 12 而不是 3。

          如何判断内存碎片是否是我的应用程序的问题?什么样的程序最容易受到影响?

          显而易见的答案是您遇到内存不足异常。

          显然,在 C++ 应用程序中检测内存碎片没有很好的可移植方法。详情请见this answer

          处理内存碎片的常用方法有哪些?

          这在 C++ 中很困难,因为您在指针中使用直接内存地址,并且您无法控制谁引用了特定的内存地址。所以重新排列分配的内存块(Java 垃圾收集器的方式)不是一种选择。

          自定义分配器可以帮助管理较大内存块中小对象的分配,并重用该块中的空闲槽。

          【讨论】:

            【解决方案9】:

            这是一个超级简化版的傻瓜。

            当对象在内存中创建时,它们会被添加到内存中已使用部分的末尾。

            如果一个不在已用内存部分末尾的对象被删除,这意味着该对象位于其他 2 个对象之间,它将创建一个“洞”。

            这就是所谓的碎片化。

            【讨论】:

              【解决方案10】:

              当您想在堆上添加一个项目时,计算机必须搜索空间以容纳该项目。这就是为什么没有在内存池或池分配器上进行动态分配时可以“减慢”事情的原因。对于繁重的 STL 应用程序,如果您正在执行多线程,则可以使用 Hoard allocatorTBB Intel 版本。

              现在,当内存碎片化时,可能会发生两种情况:

              1. 必须进行更多搜索才能找到放置“大”物体的好空间。也就是说,在某些情况下,由于许多小对象散落在各处,因此很难找到一块好的连续内存块(这些是极端情况。)
              2. 内存不是一些容易阅读的实体。处理器受限于它们可以容纳的数量和位置。如果他们需要的项目在一个地方但当前地址在另一个地方,他们会通过交换页面来做到这一点。如果您经常需要交换页面,处理速度可能会减慢(同样,这会影响性能的极端情况。)请参阅virtual memory 上的此帖子。

              【讨论】:

                【解决方案11】:

                内存碎片是因为请求了不同大小的内存块。考虑一个 100 字节的缓冲区。您请求两个字符,然后是一个整数。现在您释放这两个字符,然后请求一个新的整数 - 但该整数无法放入两个字符的空间中。该内存不能被重新使用,因为它没有足够大的连续块来重新分配。最重要的是,您为您的字符调用了很多分配器开销。

                本质上,在大多数系统上,内存仅以一定大小的块形式出现。一旦你将这些块分开,它们就不能重新加入,直到整个块被释放。当实际上只有一小部分块在使用时,这可能会导致整个块被使用。

                减少堆碎片的主要方法是进行更大、更不频繁的分配。在极端情况下,您至少可以在自己的代码中使用能够移动对象的托管堆。这完全消除了这个问题——无论如何,从内存的角度来看。显然,移动物体等是有代价的。实际上,只有当您经常从堆中分配非常少的数量时,您才会真正遇到问题。使用连续容器(向量、字符串等)并尽可能多地在堆栈上分配(对于性能来说总是一个好主意)是减少它的最佳方法。这也增加了缓存的一致性,从而使您的应用程序运行得更快。

                您应该记住的是,在 32 位 x86 桌面系统上,您有整个 2GB 的内存,它被分成 4KB 的“页面”(很确定页面大小在所有 x86 系统上都是相同的)。您将不得不调用一些 omgwtfbbq 碎片来解决问题。碎片化确实是一个过去的问题,因为现代堆对于绝大多数应用程序来说都过大,并且能够承受它的系统普遍存在,例如托管堆。

                【讨论】:

                  【解决方案12】:

                  哪种程序最容易受到影响?

                  与内存碎片相关的问题的一个很好的(=可怕的)例子是 “Elemental: War of Magic” 的开发和发布,这是一款由Stardock 开发的电脑游戏。

                  该游戏是为 32 位/2GB 内存构建的,必须在内存管理方面进行大量优化才能使游戏在 2GB 内存中运行。由于“优化”导致不断分配和取消分配,随着时间的推移,堆内存碎片发生并导致游戏崩溃每次 时间

                  YouTube 上有一个"war story" interview

                  【讨论】:

                    猜你喜欢
                    • 2011-07-11
                    • 2013-08-14
                    • 2010-10-26
                    • 2018-09-25
                    • 2012-12-11
                    • 2011-01-17
                    • 2020-02-15
                    • 1970-01-01
                    相关资源
                    最近更新 更多