之前说道alloc是原G2.9版本的默认的分配器,这篇就把alloc的原理梳理梳理,顺便简单介绍下有关的内存管理。
一般而言,我们通常习惯的内存分配操作和释放操作是这样的:
class Foo {…}; Foo* p = new Foo; // 分配内存,然后构造对象 Delete p; // 将对象析构, 然后释放内存
这里的内存分配操作由alloc::allocate()负责, 内存释放操作由alloc::deallocate()负责。
当我们申请的内存很小时,那么new(只是对malloc进行简单的包装)的空间便会形成一定程度的浪费,为什么这么说?我们先来看看基本的内存分配:
(图片来自于侯捷老师的课程《内存管理》,侵删)
当我们申请10个int对象时,内存分配的空间其实不是10 * 4 = 40个字节这么简单. 如图所示(调试状态下)最上边和最下边的两个cookie(用来记录分配空间的大小)4*2 = 8, 橙色的是Debug状态下申请出的空间帮助Debug,为32 + 4 = 36.而中间部分才为你要申请的10 * 4 = 40个字节,将这些数据加起来40 + 8+36 = 84,而pad那部分则是编译器进行自动补齐,将字节数调整成16的倍数,于是84 + 12 = 96.也就是说你只申请了10个int,而编译器却给你返回96个字节的空间,很神奇是不是?当然重点不是这里,想一下,如果我们只需要申请1个int呢?而上下两个cookie就占了8个字节,是不是很浪费?
为了考虑小型区块可能造成的浪费及内存破碎问题,SGI设计了双层级分配器,第一级分配器直接使用malloc()和free()。第二级分配器则视情况采取用不同的策略,当分配空间大于128个字节时调用一级分配器,小于128字节时,便采用更复杂的内存池管理方式,也就是alloc。
为了方便管理,SGI第二级分配器会主动将任何小额区块的内存需求量上调至8的倍数(如客端要求30字节,就自动调整为32个字节),并维护16个free-lists,各自管理大小为别为8,16,24,32,40,48,56,64,72,80,88,96,104,112,120,128字节的小额区块,free-lists的示意图如下:
(图片来自于侯捷老师的课程《内存管理》ppt,侵删)
比如我们申请32字节的空间,然后32 / 8 = 4,即应该落在3号的位置上。alloc内部并不是一次只申请这么多空间。而是一次申请需求的40倍那么大,即40 * 32 = 1280个字节。然后一半存进内存池,另一半切分为20小块, 分一块给客户,剩下的挂在5号元素上。比如再需要申请64个字节,64 / 8 = 8,挂在7号元素上,此时内存池有上次留下的1280 / 2 = 640个字节的空间. 640 / 64 = 10块,分一块给客户,剩下的挂在7号元素上。如图:
假如当时内存池空间不足时,则再申请 64 * 40 = 2560个字节的空间. 其中一半1280个字节的空间存内存池,剩下的另一半切20个小块,一块分客户,剩下的挂7号元素上面。
基于这样的原理: 每次申请一大块,慢慢切割来使用,可以减小cookie的不必要的浪费。想象一下,如果申请100万个小额空间,那么省下近800万个字节的空间。也不算小数目了。可谓为了内存利用率,无所不用其极。
然而现在G4.9版本已经不将alloc设置为默认分配器了,原因呢也不清楚。 当然了,这个alloc分配器还是存在的,毕竟这么好的东西。现在它已经在G4.9版本中换了个名字:_pool_alloc(就是内存池的意思嘛),而且也将它置于__gnu_cxx命名空间中,要是我们想用它来当做容器的分配器呢,可以这么使用:
vector<int, __gnu_cxx::_pool_alloc<int>> vec;
当然了alloc内存格局还是非常复杂的,这里我们只是简单了解下它的基本原理。如需了解详情,推荐《STL源码剖析》。相关知识点于第二章---空间置配器(allocator)。