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

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

另外:

我听说经常使用动态分配会增加内存碎片。这是真的吗?在c++环境中,我知道所有的标准容器(std::string, std::vector等)都使用动态内存分配。如果在整个程序中使用这些(特别是std::string),内存碎片更可能是一个问题吗? 在stl较多的应用程序中如何处理内存碎片?


什么是内存碎片?

内存碎片是指当您的大部分内存被分配为大量不连续的块或块时——留下很大比例的内存未分配,但在大多数典型场景下无法使用。这将导致内存溢出异常或分配错误(即malloc返回null)。

The easiest way to think about this is to imagine you have a big empty wall that you need to put pictures of varying sizes on. Each picture takes up a certain size and you obviously can't split it into smaller pieces to make it fit. You need an empty spot on the wall, the size of the picture, or else you can't put it up. Now, if you start hanging pictures on the wall and you're not careful about how you arrange them, you will soon end up with a wall that's partially covered with pictures and even though you may have empty spots most new pictures won't fit because they're larger than the available spots. You can still hang really small pictures, but most ones won't fit. So you'll have to re-arrange (compact) the ones already on the wall to make room for more..

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

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

您可能正在处理内存碎片的一个明显迹象是,如果您得到许多分配错误,特别是当已使用内存的百分比很高时(但不是您还没有使用完所有内存),那么从技术上讲,您应该有足够的空间用于您试图分配的对象。

当内存严重碎片化时,内存分配可能需要更长的时间,因为内存分配器必须做更多的工作来为新对象找到合适的空间。如果您有许多内存分配(您可能会这样做,因为您最终会产生内存碎片),分配时间甚至可能会导致明显的延迟。

处理内存碎片的常用方法是什么?

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


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

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

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

避免这种影响的常用技术包括环形缓冲区和对象池。在STL的上下文中,像std::vector::reserve()这样的方法可以提供帮助。


什么是内存碎片?

When your app uses dynamic memory, it allocates and frees chunks of memory. In the beginning, the whole memory space of your app is one contiguous block of free memory. However, when you allocate and free blocks of different size, the memory starts to get fragmented, i.e. instead of a big contiguous free block and a number of contiguous allocated blocks, there will be a allocated and free blocks mixed up. Since the free blocks have limited size, it is difficult to reuse them. E.g. you may have 1000 bytes of free memory, but can't allocate memory for a 100 byte block, because all the free blocks are at most 50 bytes long.

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

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

最明显的答案是内存不足异常。

显然,在c++应用程序中,没有一种好的便携式方法来检测内存碎片。更多细节请看这个答案。

处理内存碎片的常用方法是什么?

这在c++中很困难,因为你在指针中使用直接内存地址,你无法控制谁引用特定的内存地址。因此,重新安排已分配的内存块(Java垃圾收集器的方式)是不可取的。

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


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

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

现在,分配其中的一些(5个分配):

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

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

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

现在,尝试分配16个字节。哦,我不能,尽管有近两倍的免费。

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

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

而虚拟内存(要大得多)可能是这样的:

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

内存碎片的典型症状是,您试图分配一个大块,但您不能,即使您看起来有足够的空闲内存。另一个可能的后果是进程无法将内存释放回操作系统(因为它从操作系统中分配给malloc等进行细分的每个大块中都有一些剩余的东西,即使每个块的大部分现在都没有使用)。

Tactics to prevent memory fragmentation in C++ work by allocating objects from different areas according to their size and/or their expected lifetime. So if you're going to create a lot of objects and destroy them all together later, allocate them from a memory pool. Any other allocations you do in between them won't be from the pool, hence won't be located in between them in memory, so memory will not be fragmented as a result. Or, if you're going to allocate a lot of objects of the same size then allocate them from the same pool. Then a stretch of free space in the pool can never be smaller than the size you're trying to allocate from that pool.

一般来说,您不需要太担心它,除非您的程序是长时间运行的,并且进行了大量的分配和释放。当您同时拥有短寿命和长寿命对象时,您的风险最大,但即使在这种情况下,malloc也会尽最大努力提供帮助。基本上,忽略它,直到您的程序出现分配失败或意外地导致系统内存不足(在测试中捕获它,这是首选!)。

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


当你想在堆上添加一项时,会发生的事情是计算机必须搜索空间来容纳该项。这就是为什么动态分配不在内存池上执行或使用池分配程序会“减慢”速度的原因。对于一个沉重的STL应用程序,如果你正在做多线程,有囤积分配器或TBB英特尔版本。

现在,当内存碎片化时,会发生两件事:

There will have to be more searches to find a good space to stick "large" objects. That is, with many small objects scattered about finding a nice contigous chunk of memory could under certain conditions be difficult (these are extreme.) Memory is not some easily read entity. Processors are limited to how much they can hold and where. They do this by swapping pages if an item they need is one place but the current addresses are another. If you are constantly having to swap pages, processing can slow down (again, extreme scenarios where this impacts performance.) See this posting on virtual memory.


什么是内存碎片?

Memory fragmentation is the problem of memory becoming unusable even though it is theoretically available. There are two kinds of fragmentation: internal fragmentation is memory that is allocated but cannot be used (e.g. when memory is allocated in 8 byte chunks but the program repeatedly does single allocations when it needs only 4 bytes). external fragmentation is the problem of free memory becoming divided into many small chunks so that large allocation requests cannot be met although there is enough overall free memory.

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

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

处理内存碎片的常用方法是什么?

使用一个好的内存分配器。在IIRC中,那些使用“最佳匹配”策略的人通常在避免碎片化方面要出色得多,只是速度稍慢一些。然而,事实也表明,对于任何配置策略,都存在病态的最坏情况。幸运的是,对于分配器来说,大多数应用程序的典型分配模式实际上是相对友好的。如果你对细节感兴趣,这里有一堆文件:

Paul R. Wilson, Mark S. Johnstone, Michael Neely and David Boles. Dynamic Storage Allocation: A Survey and Critical Review. In Proceedings of the 1995 International Workshop on Memory Management, Springer Verlag LNCS, 1995 Mark S.Johnstone, Paul R. Wilson. The Memory Fragmentation Problem: Solved? In ACM SIG-PLAN Notices, volume 34 No. 3, pages 26-36, 1999 M.R. Garey, R.L. Graham and J.D. Ullman. Worst-Case analysis of memory allocation algorithms. In Fourth Annual ACM Symposium on the Theory of Computing, 1972


内存碎片是因为请求不同大小的内存块。考虑一个100字节的缓冲区。您请求两个字符,然后是一个整数。现在释放这两个字符,然后请求一个新的整数——但是这个整数不能容纳这两个字符的空间。该内存不能被重用,因为它不在一个足够大的连续块中,无法重新分配。除此之外,还为字符调用了大量分配器开销。

从本质上讲,在大多数系统上,内存只以一定大小的块形式存在。一旦你把这些块分开,它们不能重新连接,直到整个块被释放。这可能导致整个区块都在使用,而实际上只有一小部分区块在使用。

The primary way to reduce heap fragmentation is to make larger, less frequent allocations. In the extreme, you can use a managed heap that is capable of moving objects, at least, within your own code. This completely eliminates the problem - from a memory perspective, anyway. Obviously moving objects and such has a cost. In reality, you only really have a problem if you are allocating very small amounts off the heap often. Using contiguous containers (vector, string, etc) and allocating on the stack as much as humanly possible (always a good idea for performance) is the best way to reduce it. This also increases cache coherence, which makes your application run faster.

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


内存碎片与磁盘碎片是同一个概念:它指的是由于正在使用的区域没有足够紧密地打包在一起而浪费的空间。

举个简单的例子,假设你有10个字节的内存:

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

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

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

现在释放block B:

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

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

你怎么知道这是个问题?那么,最大的迹象就是程序的虚拟内存大小比实际使用的内存量大得多。在现实世界的示例中,您将拥有超过10个字节的内存,因此D将从字节9开始分配,而字节3-5将一直未使用,除非稍后分配长度为3字节或更小的内存。

在这个例子中,3个字节并不是很大的浪费,但是考虑一个更病态的情况,两个字节的分配,例如,内存中间隔10兆字节,而您需要分配一个大小为10兆字节+ 1字节的块。你必须要求操作系统提供超过10兆字节的虚拟内存,即使你只差一个字节就有足够的空间了。

How do you prevent it? The worst cases tend to arise when you frequently create and destroy small objects, since that tends to produce a "swiss cheese" effect with many small objects separated by many small holes, making it impossible to allocate larger objects in those holes. When you know you're going to be doing this, an effective strategy is to pre-allocate a large block of memory as a pool for your small objects, and then manually manage the creation of the small objects within that block, rather than letting the default allocator handle it.

In general, the fewer allocations you do, the less likely memory is to get fragmented. However, STL deals with this rather effectively. If you have a string which is using the entirety of its current allocation and you append one character to it, it doesn't simply re-allocate to its current length plus one, it doubles its length. This is a variation on the "pool for frequent small allocations" strategy. The string is grabbing a large chunk of memory so that it can deal efficiently with repeated small increases in size without doing repeated small reallocations. All STL containers in fact do this sort of thing, so generally you won't need to worry too much about fragmentation caused by automatically-reallocating STL containers.

当然,STL容器不会在彼此之间共享内存,所以如果你要创建许多小容器(而不是几个经常调整大小的容器),你可能需要像处理任何经常创建的小对象(不管是不是STL)一样,注意防止碎片化。


更新: 谷歌TCMalloc:线程缓存Malloc 已经发现它在处理长时间运行进程中的碎片方面相当出色。


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

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

About my experience. On HP-UX it is very easy to find memory fragmentation using HP-UX gdb. You set a break-point and when you hit it you run this command: info heap and see all memory allocations for the process and the total size of heap. Then your continue your program and then some time later your again hit the break-point. You do again info heap. If the total size of heap is bigger but the number and the size of separate allocations are the same then it is likely that you have memory allocation problems. If necessary do this check few fore times.

My way of improving the situation was this. After I had done some analysis with HP-UX gdb I saw that memory problems were caused by the fact that I used std::vector for storing some types of information from a database. std::vector requires that its data must be kept in one block. I had a few containers based on std::vector. These containers were regularly recreated. There were often situations when new records were added to the database and after that the containers were recreated. And since the recreated containers were bigger their did not fit into available blocks of free memory and the runtime asked for a new bigger block from the OS. As a result even though there were no memory leaks the memory consumption of the process grew. I improved the situation when I changed the containers. Instead of std::vector I started using std::deque which has a different way of allocating memory for data.

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

My understanding is that in an STL-heavy application you have first to identify problems. Memory allocators (like in libc) actually handle the problem of a lot of small allocations, which is typical for std::string (for instance in my server application there are lots of STL strings but as I see from running info heap they are not causing any problems). My impression is that you need to avoid frequent large allocations. Unfortunately there are situations when you can't avoid them and have to change your code. As I say in my case I improved the situation when switched to std::deque. If you identify your memory fragmention it might be possible to talk about it more precisely.


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

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

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

这就是所谓的碎片化。


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

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

这是11年来我一直在softwareverify.com上回答人们关于内存碎片问题的答案的高潮


什么样的程序最可能受到影响?

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

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

YouTube上有一个“战争故事”采访。