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

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

另外:

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


当前回答

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

举个简单的例子,假设你有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)一样,注意防止碎片化。

其他回答

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

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

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

假设你有一个“大”(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.

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

举个简单的例子,假设你有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)一样,注意防止碎片化。

什么是内存碎片?

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垃圾收集器的方式)是不可取的。

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