malloc
Cookie占用8个字节,所需大小比较小的话,Cookie占用的比率就比较大,造成了浪费。
各版本 allocator 实现方式;
G2.9 容器使用的分配器,不是std::allocator 而是 std::alloc
。而在G4.9里 std::alloc 变为 __pool_alloc
。
而pool allocator的思想是,对于相同大小的元素/同一类型的元素放到一整块内存中,每次只malloc这一整块内存,容器在为每个元素申请内存时只需要在申请的一整块大的内存中分割一小块即可。这一小块中不需要包含上述的cookie信息,因此就节省了很多不需要的额外内存。
std::alloc原理
一般常见的内存池是为一个类维护一个内存池,说是一个类,其实它的限制并没有那么的严谨,应该说是为了一种大小的子空间维护一个内存池,也就是说,只要每次分配的内存空间的大小相同(如容器),那么就可以使用同一个内存池为其分配内存。
std::alloc就是使用了这种思想,他并不是分散的维护一个一个的小的内存池,而是维护一个内存池的链表(长度为16),间隔8byte的维护16种不同大小的内存池。如下图所示:(图片来自侯捷C++内存分配课程讲义)
#0所对应的节点连接的链表负责分配大小为8byte的子空间
#3所对应的节点连接的链表负责分配大小为32byte的子空间
以此以8byte的间隔向后类推
#15所对应的节点连接的链表负责分配大小为128byte的子空间
当需要一次性分配的内存超过了128byte,std::alloc()本身就不会为其服务,而会将这个需求转给malloc去处理。
可是用户申请分配的空间可能并不会正好是8的倍数(通常都不是),这时,就会把他提升至最近的长度,并为其分配对应大小的内存块。
同时,在每一次需要调用malloc去分配内存时,std::alloc通常会分配比指定的大小更大的内存(至少为要求的20 * 2 倍),其中一半的作为当前的内存块进行分割并交付,剩下的将会被储存至pool。
而当我们归还内存时,也是将他连接到对应大小的内存队列头部,如果超过了128byte,也会被转交给其他函数。
最初的时候,std::alloc()会创建一个链表(代码中的free_list),并将初始值都设置为0,以后将用他们去维护16个子块大小不同的内存池(间隔为8byte)。
当我们第一次申请分配内存时,由于pool为空,所以会调用malloc去分配内存。这里假设需求的内存是32byte,那么就会调用malloc申请为pool分配32 * 20 * 2 + RoundUp = 1280的内存,然后将其中的32 * 20 的内存分割成20块,连接在#3上,并将其中的第一块交付。
RoundUp = 已经分配过的内存大小的总和 / 16;(经验值,不知理论依据)
此时pool中还剩余有640的内存
此时,我们再次申请分配64byte的内存,这时由于pool中有内存,且大于所需分配的内存,那么就在pool中切割出尽量多的子块(大小为需要的64byte),然后将第一块交付,将剩下的挂载在#7的队列上。
此时pool的剩余内存为0,下次在需要分配内存时,又会需要调用malloc去进行分配.
下边几张图片的行为和上面的行为相同:
七:这一步再次申请分配8byte大小的内存,同样切割pool,交付,连接。结束之后,pool中剩余内存为80byte。
这里再次申请了104byte的内存,但是pool中剩余的大小为80byte,不足以为其划分,于是者80byte的内存就被当成了内存碎片,由于其大小对应于#9链表,所以将他挂载到了#9链表下,然后再次调用malloc重新分配pool。
这里,我们申请72byte大小的空间,但是#8上边并未挂在内存块,pool中的大小为24也不足以分配一个空间,malloc在我们的假设中也调用失败。
于是编辑器就将pool中原有的24byte视为内存碎片,挂载到#2中。之后从#8链表开始向后寻找最近的挂在有内存块的链表,在这里是#9,取出其中的第一块,将其视为pool进行分割、交付、挂载。
问题分析
上边的流程结束之后,存在有很明显的问题:
首先,当他判定没有内存可分配从而结束程序时,至少两块的内存是可以分配的:
1、在alloc所掌握的内存中,还有很多的内存块没有被使用,只不过这些内存块的大小都小于所需要分配的内存,如果将他们拼接起来说不定足够为这次请求分配空间。
那为什么不这么做呢?答案很简单,代价太高。由于在这个架构里,多次分配、归还之后,每个链表上连接的空内存块可能并不是在物理上相邻的,但是我们要写数据需要的是物理上相连的内存,但是我们在这些内存块中去搜索物理相连的内存的难度又太大,所以并没有这么做。
2、由于每次调用malloc都会分配很多的内存,但是我们所真正需要的仅仅是其中的一小部分,那可以减少malloc所要求的内存,这样很多情况下都可以获得一块足够大的内存。
这个架构同样没有这么做,其原因在于,处于多进程的运行环境下,OS可能不仅仅跑了当前这一个程序,如果采用2的办法,我们虽然可以获取内存,但是我们也很可能完全占用OS的全部内存,那么其他的运行程序就会崩溃。ps:我个人理解中,RoundUp之所以调整的这么大,有可能与这一点也有关
其次,回收的内存并没有还给OS
正如之前的文章中描述的那样,将如此琐碎的内存还给OS是一件非常困难的事情,所以alloc中也并没有这样做。
源码
// author : Hou Jie (侯捷)
// date : 2015/11/11
// compiler : DevC++ 5.61 (MinGW with GNU 4.9.2)
//
// 說明:這是侯捷 E-learning video "C++內存管理" 的實例程式.
//
// filename : allocc.h
// 取材自 SGI STL 2.91 <stl_alloc.h>, 移植至 C language.
#include <stdlib.h> //for malloc(),realloc()
#include <stddef.h> //for size_t
#include <memory.h> //for memcpy()
//#define __THROW_BAD_ALLOC cerr << "out of memory" << endl; exit(1)
#define __THROW_BAD_ALLOC exit(1)
//----------------------------------------------
// 第1級配置器。
//----------------------------------------------
void (*oom_handler)() = 0;
void* oom_malloc(size_t n)
{
void (*my_malloc_handler)();
void* result;
for (;;) {
//不斷嘗試釋放、配置、再釋放、再配置…
my_malloc_handler = oom_handler;
if (0 == my_malloc_handler) {
__THROW_BAD_ALLOC; }
(*my_malloc_handler)(); //呼叫處理常式,企圖釋放記憶體
result = malloc(n)