本文從源代碼級別研究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_alloc有2個模板參數,_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成員之一,目的在於把對T1的allocator重綁定到T2上。由於__mt_alloc有2個模板參數,所以它的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,不會有任何效果。
所有成員函數裏,最重要的就是allocate和deallocate。
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_expect是gcc的內建函數,原型是: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字節以上的內存塊,直接調用new和delete進行分配和釋放。這個閥值是可以配置的。
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總共有5個bin,分別對應8,16,32,64,128字節。這裏__which的取值爲0到4,取決於__bytes的大小。
爲了在多線程之間管理內存,__mt_alloc給每個線程分配了一個線程id。不同於OS分配的線程id,__mt_alloc分配的線程id取值從1到4096(默認,可以配置),而且可以回收和重新分配給新線程,id值0保留給全局使用,不分配給任何線程。_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是成立的。如果__p爲0,那麼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_alloc的allocate函數裏調用的的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的一個重要特點就是把內存分配與對象構造區分開來,同時也區分了內存釋放與對象析構。函數construct和destroy分別負責構造和析構對象,而allocate和deallocate分別負責分配和釋放內存。
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;
這2個using的作用我也不太清楚,可能只是爲了強調一下吧。因爲_S_get_pool和_S_initialize_once在基類裏本來就是公有的,所以即使去掉這2個using語句也可以。
461 };