__mt_alloc源碼分析(1)

本文從源代碼級別研究mt allocator的內部實現,使用GCC 4.1.2版本的源碼,主要源文件爲庫文件<ext/mt_allocator.h>GCC源碼中的“libstdc++-v3/src/ mt_allocator.cc”。

假定讀者對mt allocator的原理已有一定的瞭解。

 

class __mt_alloc

爲避免一開始就深入到複雜的內存分配機制裏,我採用從上往下的研究方法。最頂層自然是提供給用戶使用的__mt_alloc類:

 

611    template<typename _Tp,

612        typename _Poolp = __common_pool_policy<__pool, __thread_default> >

613      class __mt_alloc : public __mt_alloc_base<_Tp>

 

每行代碼前面都加註了所在的行號,方便查閱。如果沒有特別註明文件名,所有的代碼都摘自<ext/mt_allocator.h>

__mt_alloc2個模板參數,_Tp是管理的對象類型,_Poolp是所用的內存池策略。stl裏實現的可用與__mt_alloc的內存池策略有2個,__common_pool_policy__per_type_pool_policy,前者對所有的_Tp類型一視同仁,後者會區別對待每個_Tp類型,這在後面詳細研究。__mt_alloc的基類是__mt_alloc_base

根據stl標準,每個allocator都要定義幾個固定的typedef類型:

 

616        typedef size_t                       size_type;

617        typedef ptrdiff_t                    difference_type;

618        typedef _Tp*                         pointer;

619        typedef const _Tp*                  const_pointer;

620        typedef _Tp&                         reference;

621        typedef const _Tp&                  const_reference;

622        typedef _Tp                          value_type;

 

__mt_alloc還多出來2個:

 

623        typedef _Poolp                 __policy_type;

624        typedef typename _Poolp::pool_type    __pool_type;

 

__policy_type__mt_alloc採用的內存池策略,而__pool_type則是內存池的類型。

rebind嵌套類也是stl標準規定的allocator成員之一,目的在於把對T1allocator重綁定到T2上。由於__mt_alloc2個模板參數,所以它的rebind類也有2個參數。只要你能看懂模板參數的推導,就能看懂rebind的定義。

__mt_alloc的公有成員函數接口相比stl標準的allocator也多出2個,

 

648        const __pool_base::_Tune

649        _M_get_options()

650        {

651     // Return a copy, not a reference, for external consumption.

652     return __policy_type::_S_get_pool()._M_get_options();

653        }

654       

655        void

656        _M_set_options(__pool_base::_Tune __t)

657        { __policy_type::_S_get_pool()._M_set_options(__t); }

658      };

 

分別是得到__mt_alloc的配置參數和設置配置參數。注意設置函數_M_set_options必須在任何內存分配動作之前調用,因爲第一次調用allocate()的時候__mt_alloc會進行一次參數初始化,如果你在初始化之後再_M_set_options,不會有任何效果。

所有成員函數裏,最重要的就是allocatedeallocate

allocate

660    template<typename _Tp, typename _Poolp>

661      typename __mt_alloc<_Tp, _Poolp>::pointer

662      __mt_alloc<_Tp, _Poolp>::

663      allocate(size_type __n, const void*)

 

這是allocate函數的原型,參數__n是要分配的對象的個數。

 

664      {

665        if (__builtin_expect(__n > this->max_size(), false))

666     std::__throw_bad_alloc();

 

__builtin_expectgcc的內建函數,原型是:long __builtin_expect(long exp, long c)。其第一個參數exp爲一個整型表達式,這個內建函數的返回值也是這個exp,而c爲一個編譯期常量。這個函數的語義是:你期望exp表達式的值等於常量c,從而GCC爲你優化程序,將符合這個條件的分支放在合適的地方。

max_size()函數返回“size_t(-1) / sizeof(_Tp)”,這個值在正常的運行情況下顯然比__n大,所以這裏用__builtin_expect進行一些優化。

 

668        __policy_type::_S_initialize_once();

 

這裏就是__mt_alloc初始化配置參數的地方。從函數名字可以看出來,初始化工作只會進行一次。 _S_initialize_once屬於__policy_type,在後面進行研究。

 

670        // Requests larger than _M_max_bytes are handled by operator

671        // new/delete directly.

672        __pool_type& __pool = __policy_type::_S_get_pool();

673        const size_t __bytes = __n * sizeof(_Tp);

674        if (__pool._M_check_threshold(__bytes))

675     {

676       void* __ret = ::operator new(__bytes);

677       return static_cast<_Tp*>(__ret);

678     }

 

得到內存池對象,計算實際需要的內存字節數__bytes,然後檢查__bytes是否大於一個閥值。__mt_alloc默認對於128字節以上的內存塊,直接調用newdelete進行分配和釋放。這個閥值是可以配置的。

 

680        // Round up to power of 2 and figure out which bin to use.

681        const size_t __which = __pool._M_get_binmap(__bytes);

682        const size_t __thread_id = __pool._M_get_thread_id();

 

如果__bytes在閥值以內,那麼首先找出它對應的bin索引。由於__mt_alloc只處理2的指數字節的內存塊,所以對於其他的數值也要上調至2的指數,然後交給對應的bin來處理。_M_get_binmap使用一個長度爲128(可配置)的數組,把字節數映射到bin的索引。__mt_alloc處理的最小的內存塊默認爲8字節(可配置),於是__mt_alloc總共有5bin,分別對應8163264128字節。這裏__which的取值爲04,取決於__bytes的大小。

爲了在多線程之間管理內存,__mt_alloc給每個線程分配了一個線程id。不同於OS分配的線程id__mt_alloc分配的線程id取值從14096(默認,可以配置),而且可以回收和重新分配給新線程,id0保留給全局使用,不分配給任何線程。_M_get_thread_id函數負責給當前線程分配新的線程id,如果已有,那麼直接返回這個id

 

684        // Find out if we have blocks on our freelist.  If so, go ahead

685        // and use them directly without having to lock anything.

686        char* __c;

687        typedef typename __pool_type::_Bin_record _Bin_record;

688        const _Bin_record& __bin = __pool._M_get_bin(__which);

 

由前面算出的__which,得到bin對象引用。

 

689        if (__bin._M_first[__thread_id])

 

每個bin都要維護4097個空閒塊鏈表,其中__thread_id=0對應的是全局的空閒塊鏈表,其他__thread_id對應線程自己的空閒塊鏈表。這些鏈表的首地址存放在_M_first數組裏,自然它的長度是4097。線程向__mt_alloc申請內存塊時,首先檢查自己的空閒塊鏈表是否有元素,這通過判斷_M_first[__thread_id]是否爲NULL來完成。

 

690     {

691       // Already reserved.

692       typedef typename __pool_type::_Block_record _Block_record;

693       _Block_record* __block = __bin._M_first[__thread_id];

694       __bin._M_first[__thread_id] = __block->_M_next;

 

如果線程自己的空閒鏈表有元素,那麼取出來到__block裏,然後讓鏈表頭指向下一個塊(如果有的話)。

 

696       __pool._M_adjust_freelist(__bin, __block, __thread_id);

 

在單線程情況下,_M_adjust_freelist什麼事情都不做。在多線程情況下,_M_adjust_freelist負責調整_M_free_M_used計數器,然後設置__block_M_thread_id爲當前線程的__thread_id

 

697       __c = reinterpret_cast<char*>(__block) + __pool._M_get_align();

 

準備把__block返回給用戶。注意用戶得到的並不是指向__block的指針,而是加上了一個_M_get_align(),默認情況下返回值爲8,表示__block與實際數據區的距離。跳過前面_M_adjust_freelist設置的_M_thread_id,這是每個塊都要記錄的信息,要保留到deallocate的時候使用。

 

698     }

699        else

700     {

701       // Null, reserve.

702       __c = __pool._M_reserve_block(__bytes, __thread_id);

 

如果線程自己的空閒鏈表沒有有元素,就向內存池申請。_M_reserve_block裏的詳細內容在後面研究。

 

703     }

704        return static_cast<_Tp*>(static_cast<void*>(__c));

 

最後返回給用戶申請到的內存的指針。

 

705      }

 

deallocate

內存塊的釋放過程與分配過程基本相反,__mt_alloc裏實現的代碼也比較簡單,當然,複雜的內幕都交給內存池去處理了。

 

707    template<typename _Tp, typename _Poolp>

708      void

709      __mt_alloc<_Tp, _Poolp>::

710      deallocate(pointer __p, size_type __n)

 

這是deallocate的函數原型,參數__p爲要釋放的對象首地址,__n爲對象的個數。

 

711      {

712        if (__builtin_expect(__p != 0, true))

713     {

 

顯然在多數情況下,__p != 0是成立的。如果__p0,那麼deallocate什麼都不需要做。

 

714       // Requests larger than _M_max_bytes are handled by

715       // operators new/delete directly.

716       __pool_type& __pool = __policy_type::_S_get_pool();

717       const size_t __bytes = __n * sizeof(_Tp);

718       if (__pool._M_check_threshold(__bytes))

719         ::operator delete(__p);

720       else

721         __pool._M_reclaim_block(reinterpret_cast<char*>(__p), __bytes);

722     }

723      }

 

接下來的代碼都很容易理解,計算總共的字節數,檢查是否超過了閥值。如果是則用delete釋放內存,否則使用_M_reclaim_block歸還給內存池。_M_reclaim_block的具體實現在後面研究。

 

class  __mt_alloc_base

__mt_alloc_base __mt_alloc的基類,也許你認爲它應該處理了所有__mt_alloc遺留下來的“難題”,但是結果可能令你失望:__mt_alloc_base其實異常簡單,連我在初次見到它是時候都有點吃驚。

 

560    /// @brief  Base class for _Tp dependent member functions.

 

這句話定位了__mt_alloc_base的角色,它只處理與_Tp有關的一些成員函數。

 

561    template<typename _Tp>

562      class __mt_alloc_base

563      {

564      public:

565        typedef size_t                    size_type;

566        typedef ptrdiff_t                 difference_type;

567        typedef _Tp*                      pointer;

568        typedef const _Tp*                const_pointer;

569        typedef _Tp&                      reference;

570        typedef const _Tp&                const_reference;

571        typedef _Tp                       value_type;

 

這是基本的typedef類型,與__mt_alloc裏的完全一樣。

 

573        pointer

574        address(reference __x) const

575        { return &__x; }

576 

577        const_pointer

578        address(const_reference __x) const

579        { return &__x; }

580 

581        size_type

582        max_size() const throw()

583        { return size_t(-1) / sizeof(_Tp); }

 

__mt_allocallocate函數裏調用的的max_size()就是這個函數。

 

585        // _GLIBCXX_RESOLVE_LIB_DEFECTS

586        // 402. wrong new expression in [some_] allocator::construct

587        void

588        construct(pointer __p, const _Tp& __val)

589        { ::new(__p) _Tp(__val); }

 

使用placement new操作符在__p指向的內存上建立起值爲__val _Tp對象。

 

591        void

592        destroy(pointer __p) { __p->~_Tp(); }

593      };

 

stl裏,allocator的一個重要特點就是把內存分配與對象構造區分開來,同時也區分了內存釋放與對象析構。函數constructdestroy分別負責構造和析構對象,而allocatedeallocate分別負責分配和釋放內存。

 

class __common_pool_policy

雖然只是__mt_alloc的一個模板參數,但是它的意義遠大於__mt_alloc_base。當然,__common_pool_policy也不是真正的“幕後主角”,它的作用只是提供_M_rebind,給__mt_alloc::rebind使用。

 

450    /// @brief  Policy for shared __pool objects.

451    template<template <bool> class _PoolTp, bool _Thread>

452      struct __common_pool_policy : public __common_pool_base<_PoolTp, _Thread>

 

模板參數_PoolTp是真正的內存池,_Thread表示是否需要多線程支持。

 

453      {

454        template<typename _Tp1, template <bool> class _PoolTp1 = _PoolTp,

455            bool _Thread1 = _Thread>

 

由於_M_rebind只在__mt_alloc::rebind裏使用過,而且只使用了第一個模板參數_Tp1,所以後2個模板參數其實只是爲了與__common_pool_policy自己的定義“兼容”,所以這裏用了默認值。

 

456          struct _M_rebind

457          { typedef __common_pool_policy<_PoolTp1, _Thread1> other; };

 

other的類型其實只與_PoolTp1_Thread1有關,所以_Tp1模板參數雖然是__mt_alloc::rebind唯一關心的東西,但是到了這裏,卻發現毫無用處。其實整個_M_rebind都是毫無用處的,如果你回頭看__mt_alloc::rebind定義的629行,就會發現pol_type其實就是_Poolp1自己,因爲模板參數_Tp1_M_rebind里根本就沒有用到過。

 

459        using  __common_pool_base<_PoolTp, _Thread>::_S_get_pool;

460        using  __common_pool_base<_PoolTp, _Thread>::_S_initialize_once;

 

2using的作用我也不太清楚,可能只是爲了強調一下吧。因爲_S_get_pool_S_initialize_once在基類裏本來就是公有的,所以即使去掉這2using語句也可以。

 

461    };

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章