__pool<true>的内存释放
多线程内存池__pool<true>的内存释放工作主要由函数_M_reclaim_block完成。
<mt_allocator.cc>
250 void
251 __pool<true>::_M_reclaim_block(char* __p, size_t __bytes)
函数原型,参数__p为用户释放的内存块地址,__bytes为内存块字节大小。
252 {
253 // Round up to power of 2 and figure out which bin to use.
254 const size_t __which = _M_binmap[__bytes];
255 const _Bin_record& __bin = _M_bin[__which];
找到负责的bin。
257 // Know __p not null, assume valid block.
258 char* __c = __p - _M_get_align();
259 _Block_record* __block = reinterpret_cast<_Block_record*>(__c);
记住用户数据区与内存块的首地址是有偏移的。
260 if (__gthread_active_p())
261 {
262 // Calculate the number of records to remove from our freelist:
263 // in order to avoid too much contention we wait until the
264 // number of records is "high enough".
根据注释,下面的代码采用了一种“转移策略”,在线程的空闲块个数达到一定“条件”的时候转移若干个块给全局空闲链表。读者可能还记得前面介绍过多线程下mt allocator转移数据块的原理,那里面说要把free计数器控制在used计数器的_S_freelist_headroom %以内,并且每次移动的块数为block_count个,block_count表示_S_chunk_size大小的内存能生成多少个空闲块。但是,现在我告诉你,这个算法已经“过时”了,至少下面的代码不是采用这个算法。
265 const size_t __thread_id = _M_get_thread_id();
266 const _Tune& __options = _M_get_options();
267 const unsigned long __limit = 100 * (_M_bin_size - __which)
268 * __options._M_freelist_headroom;
269
270 unsigned long __remove = __bin._M_free[__thread_id];
271 __remove *= __options._M_freelist_headroom;
272 if (__remove >= __bin._M_used[__thread_id])
273 __remove -= __bin._M_used[__thread_id];
274 else
275 __remove = 0;
276 if (__remove > __limit && __remove > __bin._M_free[__thread_id])
277 {
278 _Block_record* __first = __bin._M_first[__thread_id];
279 _Block_record* __tmp = __first;
280 __remove /= __options._M_freelist_headroom;
281 const unsigned long __removed = __remove;
这里计算出来的__removed,就是实际应该从线程里转移的块的个数。关于这个“转移策略”,我想在介绍完这段代码后专门研究,因为这里的空间显然不适合列举那些公式。
282 while (--__remove > 0)
283 __tmp = __tmp->_M_next;
284 __bin._M_first[__thread_id] = __tmp->_M_next;
285 __bin._M_free[__thread_id] -= __removed;
数出__removed个块,剩余的还由__bin._M_first[__thread_id]保管。
287 __gthread_mutex_lock(__bin._M_mutex);
288 __tmp->_M_next = __bin._M_first[0];
289 __bin._M_first[0] = __first;
290 __bin._M_free[0] += __removed;
291 __gthread_mutex_unlock(__bin._M_mutex);
把__removed个块加入到全局空闲链表里。注意这里需要给bin加锁,因为要修改bin里的全局数据。
292 }
293
294 // Return this block to our list and update counters and
295 // owner id as needed.
296 --__bin._M_used[__block->_M_thread_id];
__block指向的是用户现在归还的那个块,现在把分配这个块的线程的used计数器减1。由于内存块可能在线程间传递,所以__block->_M_thread_id可能不是当前线程的id。虽然我不是很肯定,但是这里显然使用了一个假设:第296行代码是原子的执行的。
298 __block->_M_next = __bin._M_first[__thread_id];
299 __bin._M_first[__thread_id] = __block;
300
301 ++__bin._M_free[__thread_id];
把用户释放的块加入当前线程的空闲链表里。
302 }
303 else
304 {
305 // Not using threads, so single threaded application - return
306 // to global pool.
307 __block->_M_next = __bin._M_first[0];
308 __bin._M_first[0] = __block;
如果不支持多线程,直接把块归还到全局空闲链表里。
309 }
310 }
空闲块的转移策略
现在我们回头看__pool<true>::_M_reclaim_block里的转移策略是什么。我剔除了所有进行链表操作的代码,而只留下了参数计算的部分,那么代码就变成这样了:
<mt_allocator.cc>
267 const unsigned long __limit = 100 * (_M_bin_size - __which)
268 * __options._M_freelist_headroom;
270 unsigned long __remove = __bin._M_free[__thread_id];
271 __remove *= __options._M_freelist_headroom;
272 if (__remove >= __bin._M_used[__thread_id])
273 __remove -= __bin._M_used[__thread_id];
274 else
275 __remove = 0;
276 if (__remove > __limit && __remove > __bin._M_free[__thread_id])
277 {
280 __remove /= __options._M_freelist_headroom;
281 const unsigned long __removed = __remove;
285 __bin._M_free[__thread_id] -= __removed;
290 __bin._M_free[0] += __removed;
291
292 }
根据这部分代码,我可以用数学公式的形式写出转移策略:
1)初始定义:
F:free计数器的值;
U:used计数器的值;
B:_M_bin_size,即bin的个数;
W:__which,即当前bin的索引,取值范围[0, B-1];
H:_M_freelist_headroom,用户可调的参数。
2)公式推导:
计算limit
L = 100 * (B - W) * H = 100H(B-W)
计算remove
R = F * H - U = FH-U
根据代码,如果FH < U,则R = 0。这种情况我们可以忽略,因为我们关心的是什么时候需要转移,以及应该转移多少。
接下来根据2个条件判断是否应该转移。
条件①
R > L => FH-U > 100H(B-W) => F > 100(B-W) + U/H = F1
条件②
R > F => FH-U > F => F > U/(H-1) = F2
如果条件①和②都满足,那么,计算需要转移的块的个数removed
D = R / H = (FH-U)/H = F-U/H
计算剩余块的个数
L = F - D = U/H
我们首先弄清楚H(也就是可调参数_M_freelist_headroom)到底是什么意义。从剩余块L的计算公式可以看到,无论F是多少,只要发生了块转移,那么最终线程里剩余的块只与U(used计数器)和H有关,而且剩余量是:
U/H = (100 / H) % * U
这个算法试图把free计数器控制在used计数器值的(100 / _M_freelist_headroom)%以内,而不是以前说的_M_freelist_headroom%以内。
同时我们也看到,块转移并不是在F超过L的时候马上进行的,而是必须满足条件①和②,为什么呢?我们从需要转移的块的个数着手:
根据条件①和②,和D的计算公式,有
D > F1 - U/H = 100(B-W) + U/H - U/H = 100(B-W) = D1,且
D > F2 - U/H = U/(H-1) - U/H = U/(H(H-1)) = D2
这说明只有当可转移的块个数达到一定量的时候,转移操作才会进行。D1基本是一个bin的常数,比如8到128字节的5个bin,各自的D1分别为500,400,300,200,100,对于块越小的bin,D1的值越大。D2是与used计数器和_M_freelist_headroom相关的值。可以看出used有一个阀值,在这个值以下的时候,必须用D1来防止“不必要”的转移。可以算出这个阀值是:
D1 = D2 => UT = 100(B-W)H(H-1)
当U > UT时,条件②会起作用,同时D2也会起作用。下表列出了一个计算实例,给读者一些“感官印象”。
U |
B-W |
H |
F1 |
F2 |
UT |
L |
D |
0 |
1 |
5 |
100 |
0 |
2000 |
0 |
100 |
500 |
1 |
5 |
200 |
125 |
2000 |
100 |
100 |
1000 |
1 |
5 |
300 |
250 |
2000 |
200 |
100 |
1500 |
1 |
5 |
400 |
375 |
2000 |
300 |
100 |
2000 |
1 |
5 |
500 |
500 |
2000 |
400 |
100 |
2500 |
1 |
5 |
600 |
625 |
2000 |
500 |
125 |
3000 |
1 |
5 |
700 |
750 |
2000 |
600 |
150 |
3500 |
1 |
5 |
800 |
875 |
2000 |
700 |
175 |
4000 |
1 |
5 |
900 |
1000 |
2000 |
800 |
200 |
4500 |
1 |
5 |
1000 |
1125 |
2000 |
900 |
225 |
5000 |
1 |
5 |
1100 |
1250 |
2000 |
1000 |
250 |
从上表可以看出,当_M_freelist_headroom(H)为5时,每次转移之后剩余块数L都是used计数器值(U)的20%。在U的值小于UT(=2000)的时候,F1的值小于F2,条件①起主要作用,此时每次至少转移100(=100(B-W))个块;当U大于UT的时候,条件②起主要作用,每次转移的块数逐步增多。