多线程的内存分配器mt_alloc

A fixed-size, multi-thread optimized allocator

原文URLhttp://list.cs.brown.edu/people/jwicks/libstdc++/html/ext/mt_allocator.html

简介

mt allocator是一个固定大小(2的幂)内存的分配器,最初是为多线程应用程序(以下简称为MT程序)设计的。经过多年的改进,现在它在单线程应用程序(以下简称为ST程序)里也有出色的表现了。

本文的目的是从应用程序的角度描述mt allocator的“内幕”。  

总体设计

mt allocator3个组成部分:描述内存池特征的参数,把内存池关联到通用或专用方案的policy类,和从policy类继承来的实际的内存分配器类。

描述内存池特征的参数是:

template<bool _Thread>

     class __pool

这个类表示是否支持线程,然后对多线程(bool==true)和单线程(bool==false)情况进行显式特化。可以用定制的参数来替代这个类。

对于policy类,至少有2种不同的风格,每种都可以和上面不同的内存池参数单独搭配:第一个策略,__common_pool_policy,实现了一个通用内存池,即使分配的对象类型不同,比如charlong,也使用同一个的内存池。这是默认的策略。

template < bool  _Thread >
    
struct  __common_pool_policy

  template
< typename _Tp,  bool  _Thread >
    
struct  __per_type_pool_policy

 第二个策略,__per_type_pool_policy,对每个对象类型都实现了一个单独的内存池,于是charlong会使用不同的内存池。这样可以对某些类型进行单独调整。

把上面这些放到一起,我们就得到了实际的内存分配器:

template < typename _Tp, typename _Poolp  =  __default_policy >
    
class  __mt_alloc :  public  __mt_alloc_base < _Tp > ,  _Poolp

这个类有标准库要求的接口,比如allocatedeallocate函数等。 

可调参数

有些配置参数可以修改,或调整。有一个嵌套类:包含了所有可调的参数,即:

struct  __pool_base::_Tune

 

l  字节对齐

l  多少字节以上的内存直接用new分配

l  可分配的最小的内存块大小

l  每次从OS申请的内存块的大小

l  可支持的最多线程数目

l  单个线程能保存的空闲块的百分比(超过的空闲块会归还给全局空闲链表)

l  是否直接使用newdelete

对这些参数的调整必须在任何内存分配动作之前,即内存分配器初始化的时候,比如: 

#include  < ext / mt_allocator.h >

struct  pod
{
  
int i;
  
int j;
}
;

int  main()
{
  typedef pod value_type;
  typedef __gnu_cxx::__mt_alloc
<value_type> allocator_type;
  typedef __gnu_cxx::__pool_base::_Tune tune_type;

  tune_type t_default;
  tune_type t_opt(
1651203251202010false);
  tune_type t_single(
165120325120110false);

  tune_type t;
  t 
= allocator_type::_M_get_options();  
  allocator_type::_M_set_options(t_opt);
  t 
= allocator_type::_M_get_options();  

  allocator_type a;
  allocator_type::pointer p1 
= a.allocate(128);
  allocator_type::pointer p2 
= a.allocate(5128);

  a.deallocate(p1, 
128);
  a.deallocate(p2, 
5128);

  
return 0;
}

初始化

静态变量(内存链表的指针,控制参数等)的初始化为默认的值,比如: 

template < typename _Tp >  size_t
__mt_alloc
< _Tp > ::_S_freelist_headroom  =   10 ;

首次调用allocate()时,也会调用_S_init()函数。为了保证在MT程序里它只被调用一次,我们使用了__gthread_once(参数是_S_once_mt_S_init)函数;在ST程序里则检查静态bool变量_S_initialized

_S_init()函数:如果设置了GLIBCXX_FORCE_NEW环境变量,它会把_S_force_new设置成true,这样allocate()就直接用new来申请内存,deallocate()delete来释放内存。

如果没有设置GLIBCXX_FORCE_NEWSTMT程序都会:

1)计算bin的个数。bin是指2的指数字节的内存集合。默认情况下,mt allocator只处理128字节以内的小内存分配(或者通过在_S_init()里设置_S_max_bytes来更改这个值),这样就有如下几个字节大小的bin1248163264128

2)创建_S_binmap数组。所有的内存申请都上调到2 的指数大小,所以29字节的内存申请会交给32字节的bin处理。_S_binmap数组的作用就是快速定位到合适的bin,比如数值29被定位到5bin 5 = 32字节)。

3)创建_S_bin数组。这个数组由bin_record组成,数组的长度就是前面计算的bin的个数,比如,当_S_max_bytes = 128时长度为8

4)初始化每个bin_recordfirst<block_record *>数组,程序可以有多少个线程,这个数组就有多长(ST程序只有1个线程,MT程序最多允许_S_max_threads个线程)。first里保存的是这个bin里每个线程第一个空闲块的地址,比如,我们要找线程332字节的空闲块,只需调用:_S_bin[ 5 ].first[ 3 ]。开始的时候first数组元素全是NULL

对于MT程序,还要进行下面的工作:

5)创建一个空闲线程ID1_S_max_threads间的一个数值)的列表,列表的入口是_S_thread_freelist_first。由于__gthread_self()函数返回的不是我们需要的1_S_max_threads之间的数值,而是类似于进程ID的随机数,所以我们需要创建一个thread_record链表,长度为_S_max_threads,每个thread_record元素的thread_id字段依次初始化成123,直到_S_max_threads,作为4)步中first的索引。当一个线程调用allocate()deallocate()时,我们会调用_S_get_thread_id(),检查线程本地存储的变量_S_thread_key的值。如果是NULL则表示是新创建的线程,那么从_S_thread_freelist_first列表里拿出一个元素给该线程。下次调用_S_get_thread_id()时就会找到这个对象,并且找到thread_id字段和对应的bin位置。所以,首先调用allocate()的线程会分配到thread_id=1thread_record,于是它的bin索引就是1,我们可以用_S_bin[ 5 ].first[ 1 ]来为它获取32字节的空闲内存。当创建_S_thread_key时我们定制了析构函数,这样当线程退出后,它的thread_record会归还给_S_thread_freelist_first,以便重复使用。_S_thread_freelist_first链表有锁保护,在增、删元素的时候加锁。

6)初始化每个bin_record 的空闲和使用的块数计数器。bin_record->freesize_t 的数组,记录每个线程空闲块的个数。bin_record->used也是size_t 的数组,记录每个线程正在使用的块的个数。这些数组的元素初始值都是0

7)初始化每个bin_record的锁。bin_record->mutex用来保护全局的空闲块链表,每当有内存块加入或拿出某个bin时,都要进行加锁。这种情况只出现在线程需要从全局空闲链表里获取内存,或者把一些内存归还给全局链表的时候。 

单线程模型(简化的多线程模型)

我们从空闲块链表的内存布局开始。下面是bin 3里线程号为3的空闲链表的头2个块:

 ST程序里,所有的操作都在全局内存池里——即thread_id0MT程序里任何线程都不会分配到这个id)。

当程序申请内存(调用allocate()),我们首先看申请的内存大小是否大于_S_max_bytes,如果是则直接用new

否则通过_S_binmap找出合适的bin。查看一下_S_bin[ bin ].first[ 0 ]就能知道是否有空闲的块。如果有,那么直接把块移出_S_bin[ bin ].first[ 0 ],返回数据的地址。

如果没有空闲块,就需要从系统申请内存,然后建立空闲块链表。已知block_record的大小和当前bin管理的块的大小,我们算出申请的内存能分出多少个块,然后建立链表,并把第一个块的数据返回给用户。

内存释放的过程同样简单。先把指针转换回block_record指针,根据内存大小找到合适的bin,然后把块加到空闲列表的前面。

通过一系列的性能测试,我们发现“加到空闲列表前面”比加到后面有10%的性能提升。 

多线程模型

ST程序里从来用不到thread_id变量,那么现在我们从它的作用开始介绍。

向共享容器申请或释放内存的MT程序有“所有权”的概念,但有一个问题就是线程只把空闲内存返回到自己的空闲块链表里。(比如一个线程专门进行内存的申请,然后转交给其他线程使用,那么其他线程的空闲链表会越来越长,最终导致内存用尽)

每当一个块从全局链表(没有所有权)移到某个线程的空闲链表时,都会设置thread_id。其他需要设置thread_id的情况还包括直接从空白内存上建立某个线程的空闲块链表,和释放某个块删除时,发现申请块的线程id和执行释放操作的线程id不同的时候。

那么到底thread_id有什么用呢?当释放块时,我们比较块的thread_id和当前线程的thread_id是否一致,然后递减生成这个块的线程的used变量,确保freeused计数器的正确。这是很重要的,因为它们决定了是否需要把内存归还给全局内存池。

当程序申请内存(调用allocate()),我们首先看申请的内存大小是否大于_S_max_bytes,如果是则直接用new

否则通过_S_binmap找出合适的bin_S_get_thread_id()返回当前线程的thread_id,如果这是第一次调用allocate(),线程会得到一个新的thread_id,保存在_S_thread_key里。

查看_S_bin[ bin ].first[ thread_id ]能知道是否有空闲的内存块。如果有,则移出第一个块,返回给用户,别忘了更新usedfree计数器。

如果没有,我们先从全局链表(freelist (0))里寻找。如果找到了,那么把当前bin锁住,然后从全局空闲链表里移出最多block_count(从OS申请的一个内存块能生成多少个当前bin的块)个块到当前线程的空闲链表里,改变它们的所有权,更新计数器和指针。接着把bin解锁,把_S_bin[ bin ].first[ thread_id ]里第一个块返回给用户。

最多只移动block_count个块的原因是,降低后续释放块请求可能导致的归还操作(通过_S_freelist_headroom来计算,后面详述)。

如果在全局链表里也没有空闲块了,那么我们需要从OS申请内存。这和ST程序的做法一样,只有一点注意区别:从新申请的内存块(大小为_S_chunk_size字节)上建立起来的空闲链表直接交给当前进程,而不是加入全局空闲链表。

释放内存块的基本操作很简单:把内存块直接加到当前线程的空闲链表里,更新计数器和指针(前面说过如果当前线程的id和块的thread id不一致的情况下该如何处理)。随后freeused计数器就要发挥作用了,即空闲链表的长度(free)和当前线程正在使用的块的个数(used)。

让我们回想前面一个线程专门负责分配内存的程序模型。假设开始时每个线程使用了51232字节的块,那么他们的used计数器此时都是516。负责分配内存的线程接着又得到了100032字节的块,那么此时它的used计数器是1516

如果某个线程释放了500个块,每次释放操作都会导致used计数器递减,和该线程的空闲链表(free)越来越长。不过deallocate()会把free控制在used_S_freelist_headroom%以内(默认是10%),于是当free超过52516 / 10)时,释放的空闲块会归还给全局空闲链表,从而负载分配的线程就能重用它们。

为了减少锁竞争(这种归还操作需要对bin进行加锁),归还操作是以block_count个块为单位进行的(和从全局空闲链表里获得块一样)。这个“规则”还可以改进,减少某些块“来回转移”的几率。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值