__mt_alloc源码分析(11)

探讨了多线程环境下__pool<true>内存池的内存释放机制,详细解析了_M_reclaim_block函数的工作流程及空闲块的转移策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

__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)初始定义:

           Ffree计数器的值;

           Uused计数器的值;

           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是多少,只要发生了块转移,那么最终线程里剩余的块只与Uused计数器)和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的常数,比如8128字节的5bin,各自的D1分别为500400300200100,对于块越小的binD1的值越大。D2是与used计数器和_M_freelist_headroom相关的值。可以看出used有一个阀值,在这个值以下的时候,必须用D1来防止“不必要”的转移。可以算出这个阀值是:

           D1 = D=>  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_headroomH)为5时,每次转移之后剩余块数L都是used计数器值(U)的20%。在U的值小于UT=2000)的时候,F1的值小于F2,条件起主要作用,此时每次至少转移100=100(B-W))个块;当U大于UT的时候,条件起主要作用,每次转移的块数逐步增多。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值