[侯捷 C++内存管理] 标准分配器实现
VC6 标准分配器之实现
VC6 malloc()
我们现在希望能够去除上下的cookie 以节省内存
去除cookie 的先决条件是区块要一样大小。容器里所有元素是同样的大小
VC6 allocator
VC6:哎,我就是不做内存管理设计,我就是玩
VC6 的 allocator 没做任何的内存管理,只是把malloc 换一个样貌。就是用::operator new 和::operator delete 完成任务罢了
VC里所有的容器的第二个模板参数都是这个偷懒的allocator ,我们使用容器最终的元素分配都是靠malloc 去做的,具体是以指定的元素类型为单位。
BC5 标准库分配器之实现
BC5和VC6的标准库分配器实现基本完全一样
G2.9 分配器的实现
G2.9的标准分配器和前面两个并没有太大区别,但是要注意到。defalloc.h 里的标准库分配器并没有被任何容器包含,这很奇怪。
我们惊讶地发现,容器用的并不是std::allocator 而是std::alloc
alloc 是一个class ,我们直接以alloc::allocate() 的方式调用,说明allocate() 和deallocate() 是allocate 里的静态函数。
另外值得注意的是,alloc::allocate(512) 接受的是字节而非元素。
而今安在哉
到了G4.9 我貌似找不到G2.9 里的std::alloc 了,这个好东西难道被去掉了吗?并没有,只是换了一个名字而已。G4.9 标准库里有许多extented allocators 其中的_pool_alloc 就是G2.9 的`alloc的化身
vector<string,__gun_cxx::__pool_alloc<string>>vec;
G4.9 标准库分配器之实现
先前我们已经看到,G2.9 的std::alloc 到了G4.9 变成体制外的__pool_alloc 了。那么,G4.9 体制内的标准分配器有什么变化呢?
G4.9 的std::allocator 虽然多了个继承,但是换汤不换药啊(脑内开始范大将军名场面),还是没有做啥特别设计
G4.9 pool allocator 用例
pool allocator 相比std::allocator 的好处就是可以消除cookie 。
试想一下,一个元素cookie 为8 bytes 那么一百万个元素呢?省下的内存可不小啊!
cout<<sizeof(__gun_cxx::__pool_alloc<int>)<<endl;
G2.9 std::alloc 运行模式
16条链表分别负责8、16、24、32……字节,如果容器发出来的需求不是8的倍数,那么会调整到8的倍数。负责范围[8-128],如果超过了这个范围,那么就会调用malloc 来负责(malloc会有cookie ,std::alloc 没有cookie 。
如果选了32字节的,那就挖一大块(20个,不要问为什么,或许是经验值)来作分割。实际上看到左边那个32和64的free lit 我们可以看到他们是连着的,这意味着挖的时候其实挖了2*20个,一半用来分割,另一半的先不作处理做准备
embedded pointers
如果,客户(容器)要这块,那么链表的第一块就给出去。给出去的时候,容器就开始把这一块当作元素空间往里面填值,覆盖掉里面的指针(前四个字节)。并且给的时候free list 的头已经给到下一块了。
等到容器归还内存的时候,又把前面的四个字节当作指针
但是如果对象小于四个字节,就不能被借用了,不过这样的情况比较少
G2.9 std::alloc 运行一瞥
分配器的客户:容器
具体动作:看pool ,先把pool 里的挖到链表里。如果还不够就再去索求申请,注入pool
一帆风顺
STEP1:容器申请 32bytes,我们先看用于准备的pool 。由于pool 为空,所以我们用malloc 索取(注意图里的cookie )并向pool 注入
32
?
20
?
2
+
R
o
u
n
d
U
p
(
0
>
>
4
)
=
1280
32*20*2+RoundUp(0>>4)=1280
32?20?2+RoundUp(0>>4)=1280,其中
R
o
u
n
d
U
p
(
)
RoundUp()
RoundUp()是用来调整到
16
16
16 的边界的追加量,而且这个追加量会越来越大。
然后前
19
19
19个分到free list 里,剩下1个用于备用。
STEP2:接着上一步,我们接下来申请 64 bytes。先看 pool ,因为STEP1之后pool 里有 640 bytes ,所以将其分为
640
/
64
=
10
640/64=10
640/64=10个内存块,第一个给客户,剩下九个挂到list#7 。注意list#7 的不是malloc 的所以没有cookie
STEP3:接着上一步,我们接下来申请 96 bytes。先看 pool ,因为STEP2之后pool 里有 0 bytes ,所以我们用malloc 索取并向pool 注入
96
?
20
?
2
+
R
o
u
n
d
U
p
(
1280
>
>
4
)
=
3920
96*20*2+RoundUp(1280>>4)=3920
96?20?2+RoundUp(1280>>4)=3920。其中追加量,RoundUp(
目
前
的
累
计
申
请
量
<
<
4
目前的累计申请量<<4
目前的累计申请量<<4)会越来越多,这会导致一个安全阀门。其中19个区块给list#11 ,1个给用户,剩下2000备用。
STEP4:申请88,应该找第
88
/
8
?
1
=
10
88/8-1=10
88/8?1=10号链表,由于pool 剩下2000 。所以取pool 划分20个区块(尽量多但不能超20),第1个个给客户,剩下19个挂到list#10 .pool 剩下
2000
?
88
?
20
=
240
2000-88*20=240
2000?88?20=240
STEP5:连续 3 次 申请 88。直接从 #10 链表给客户
STEP6:申请8,由于 pool 有余量,所以取 pool 划分 20 个区块,1个给客户,剩下19个挂于 list #0
看了上面几张图片可能会迷惑我们的pool 怎么跑来跑去的很麻烦啊,实际上我们写代码实现的时候只需要两个指针一指就可以了
STEP7:申请
104
104
104,list #12 没有区块,pool 余量又不足提供一个(成碎片了),于是先将pool 剩下的碎片(
80
80
80)分给list #9 。然后索取并成功获得
104
?
20
?
2
+
R
o
u
n
d
U
p
(
5200
>
>
4
)
104*20*2+RoundUp(5200>>4)
104?20?2+RoundUp(5200>>4),切出
19
19
19 个给list #12 最前面的那个给客户,剩下 2048 备用
STEP8:申请
112
112
112,由于pool 有余量,从pool 中取
20
20
20 个区块,
1
1
1个给客户,留
19
19
19个挂到list #13
STEP9:申请
48
48
48,由于pool 有余量,从pool 中取
3
3
3个区块,
1
1
1个给客户,
2
2
2个挂到list #5
遭遇苦难
STEP10:申请
72
72
72,pool 余量不足以供应
1
1
1个,于是先处理碎片把pool 给list #2 。然后索取
72
?
20
?
2
+
R
o
u
n
d
U
p
(
9688
>
>
4
)
72*20*2+RoundUp(9688>>4)
72?20?2+RoundUp(9688>>4),但是现在已经没法从sysytem heap 索取那么多了,于是alloc 已经申请的资源里找最接近 的
80
80
80 (list #9 )回填给pool ,然后切出
72
72
72给客户,剩下
8
8
8
STEP11:申请
72
72
72,pool 余量不足以供应
1
1
1个,于是先处理碎片把pool 给list #0 。然后索取
72
?
20
?
2
+
R
o
u
n
d
U
p
(
9688
>
>
4
)
72*20*2+RoundUp(9688>>4)
72?20?2+RoundUp(9688>>4),但是现在已经没法从sysytem heap 索取那么多了,于是alloc 已经申请的资源里找最接近 的
88
88
88 (list #9 已经空啦,所以是list #10 )回填给pool ,然后切出
72
72
72给客户,剩下
16
16
16
山穷水尽
STEP13:申请
120
120
120,pool 余量不足以供应
1
1
1个,于是先处理碎片把pool 给list #1 。然后索取
72
?
20
?
2
+
R
o
u
n
d
U
p
(
9688
>
>
4
)
72*20*2+RoundUp(9688>>4)
72?20?2+RoundUp(9688>>4),但是现在已经没法从sysytem heap 索取那么多了,于是alloc 已经申请的资源里找最接近的,发现没有(list #14 和list #15 都是空的)结束了
柳暗花明?
std::alloc 在 STEP12 就投降举白旗了。这另我们感到诧异,明明还有出路啊。
- 我们已经拿的资源里还有那么多没有的小区块,为什么不把他们合并了?技术上好像挺难的
system heap 还剩
312
312
312为什么不拿来用?将索取量不断折半直到可以申请
蓦然回首
来回顾一下std::alloc 流程图吧
G2.9 std::alloc 源码剖析
暂时放一放实现细节
G2.9 std::alloc 细节讨论
小细节
部分代码可读性很差
#208 、#218 、#255 的写法很奇怪,但实际上是值得学习的。这样写可以避免把== 写成= ,把字面量放左边可以让编译器帮你检查
大灾难
上面都是一些细节问题,现在看到#212-#214 这块描述的是malloc 失败的时候(柳暗花明的那节)的处理
注释的意思是,如果malloc 失败了,会从已有的资源里找到最接近需求的拿来用。
但是我们不尝试更小的请求,因为这会在多进程机器里导致大灾难。
更小的请求也就是我们先前的疑惑:system heap`还剩
312
312
312为什么不拿来用?将索取量不断折半直到可以申请。
那么这个大灾难是什么?为什么说是多进程机器?
std::alloc 的策略实际上是一种竭泽而渔的方法,这在单进程的情况下确实没啥大问题。但是多进程机器中并不是只有这一个进程,竭泽而渔会导致其他进程没法正常运行,现在想来
R
o
u
d
U
p
RoudUp
RoudUp 附加量的不断增长大概也是为了这个吧。
物归原主
由于dealloc 的先天缺陷,没有调用free 或delete 将内存还给操作系统。
先天缺陷:std::alloc 成功的去掉了cookie 节省了内存,但同时也因为cookie 的缺失,我们没法知道应该释放多少内存了,也就没法把内存还给操作系统
后面我们会在loki 的分配器里看到更好的设计
|