1. 程式人生 > >stl allocator原始碼學習

stl allocator原始碼學習

概述

介紹幾個allocator的原始碼實現:簡單的對operator new和operator delete進行封裝的實現,vs2015中的實現,STLport中的實現,仿造STLport實現記憶體池。

1. 參考

http://www.cplusplus.com/reference/memory/allocator/
《STL原始碼剖析》
《C++ Primer 第五版》
《Generic Programming and the STL》(《泛型程式設計和STL》)
MSDN

2. 介紹

std::allocator是STL容器使用的記憶體配置器,也是標準庫唯一預定義的記憶體配置器。

3. 實現一:最簡單的實現

3.1 程式實現

template<class T>
class allocator
{
public:
    // 1、為什麼需要下面這些成員,有什麼作用呢?
    typedef T          value_type;
    typedef T*         pointer;
    typedef const T*   const_pointer;
    typedef T&         reference;
    typedef const T&   const_reference;
    typedef size_t     size_type;       // size_t是無符號整數
    // ptrdiff_t是有符號整數,代表指標相減結果的型別
    typedef ptrdiff_t  difference_type;

    // 2、這是做什麼用的,為何template是U,而不是與allocator的T一致?
    template<class U>
    struct rebind
    {
        typedef allocator<U> other;
    };

    // 預設建構函式,什麼都不做
    allocator() noexcept
    {
    }

    // 泛化的建構函式,也什麼都不做
    // 3、為什麼需要這個泛化的建構函式,不同型別的allocator複製合適嗎?
    template<class U>
    allocator(const allocator<U>&) noexcept
    {
    }

    // 解構函式,什麼都不做
    ~allocator() noexcept
    {
    }

    // 返回物件地址
    pointer address(reference val) const noexcept
    {
        //non-const版呼叫const版,參見《Effective C++》條款3
        return const_cast<reference>(address(static_cast<const_reference>(val)));
    }

    // 返回物件地址
    const_pointer address(const_reference val) const noexcept
    {
        return &val;
    }

    // 申請記憶體,count是指物件個數,不是位元組數。
    // 4、hint是做什麼的?
    pointer allocate(size_type count, allocator<void>::const_pointer hint = nullptr)
    {
        return static_cast<pointer>(::operator new(count * sizeof(value_type)));
    }

    // 釋放記憶體
    void deallocate(pointer ptr, size_type count)
    {
        ::operator delete(ptr);
    }

    // 可配置的最大量(指物件個數,不是位元組數)
    size_type max_size() const noexcept
    {
        return (static_cast<size_type>(-1) / sizeof(value_type));
    }

    // 構造物件,Args是模板引數包,見《C++ Primer》第5版16.4節
    template <class U, class... Args>
    void construct(U* p, Args&&... args)
    {
        ::new ((void *)p) U(::std::forward<Args>(args)...);
    }

    // 析構物件
    template <class U>
    void destroy(U* p)
    {
        p->~U(); // 原來模板還可以這樣用
    }
};

// 5、為什麼要對void進行特化?
template<>
class allocator<void>
{
public:
    typedef void value_type;
    typedef void *pointer;
    typedef const void *const_pointer;
    template <class U> struct rebind
    {
        typedef allocator<U> other;
    };
};

3.2 問題解答

1、STL的規範,同時這些type在迭代器和traits技術中有用。

2、摘自MSDN:A structure that enables an allocator for objects of one type to allocate storage for objects of another type.
This structure is useful for allocating memory for type that differs from the element type of the container being implemented.
The member template class defines the type other. Its sole purpose is to provide the type name allocator<_Other>, given the type name allocator<Type>.

For example, given an allocator object al of type A, you can allocate an object of type _Other with the expression:
A::rebind<Other>::other(al).allocate(1, (Other *)0)
Or, you can name its pointer type by writing the type:
A::rebind<Other>::other::pointer

具體例子:一個儲存int的列表list<int>,列表儲存的物件並不是int本身,而是一個數據結構,它儲存了int並且還包含指向前後元素的指標。那麼,list<int, allocator<int>>如何知道分配這個內部資料結構呢?畢竟allocator<int>只知道分配int型別的空間。這就是rebind要解決的問題。通過allocator<int>::rebind<_Node>()你就可以創建出用於分配_Node型別空間的分配器了。

3、allocator類的模板引數只有一個,代表分配的元素型別,如果allocator封裝的僅是記憶體的分配策略而與元素型別無關,定義泛型複製構造好像沒什麼不合理,同時如果不定義成泛型rebind將無法使用。construct成員函式和destroy成員函式也是泛型,allocator的使用條件還是特別寬鬆的。

4、hint
Either 0 or a value previously obtained by another call to allocate and not yet freed with deallocate.
When it is not 0, this value may be used as a hint to improve performance by allocating the new block near the one specified. The address of an adjacent element is often a good choice.

5、只有void *變數,沒有void變數,沒有void&變數,不能typedef void value_type等等。

4. 實現二:vs2015中的實現

4.1 程式實現(部分和實現一類似的內容省略)

template<class _Ty>
	class allocator
	{	// generic allocator for objects of class _Ty
public:
	static_assert(!is_const<_Ty>::value,
		"The C++ Standard forbids containers of const elements "
		"because allocator<const T> is ill-formed.");

……

	template<class _Other>
		allocator<_Ty>& operator=(const allocator<_Other>&)
		{	// assign from a related allocator (do nothing)
		return (*this);
		}

	void deallocate(pointer _Ptr, size_type _Count)
		{	// deallocate object at _Ptr
		_Deallocate(_Ptr, _Count, sizeof (_Ty));
		}

	__declspec pointer allocate(size_type _Count)
		{	// allocate array of _Count elements
		return (static_cast<pointer>(_Allocate(_Count, sizeof (_Ty))));
		}

	__declspec pointer allocate(size_type _Count, const void *)
		{	// allocate array of _Count elements, ignore hint
		return (allocate(_Count));
		}

……
	};

4.2 問題解釋

1、static_assert :Tests a software assertion at compile time. If the specified constant expression is false, the compiler displays the specified message and the compilation fails with error C2338; otherwise, the declaration has no effect.

2、__declspec :Microsoft Specific. Tells the compiler not to insert buffer overrun security checks for a function.

3、這裡的實現也比較簡單,只是對記憶體分配函式(_Allocate)和釋放函式(_Deallocate)進行簡單的封裝。

4.3 _Allocate和_Deallocate的實現

直接複製過來的實現如下

inline
	_DECLSPEC_ALLOCATOR void *_Allocate(size_t _Count, size_t _Sz,
		bool _Try_aligned_allocation = true)
	{	// allocate storage for _Count elements of size _Sz
	void *_Ptr = 0;

	if (_Count == 0)
		return (_Ptr);

	// check overflow of multiply
	if ((size_t)(-1) / _Sz < _Count)
		_Xbad_alloc();	// report no memory
	const size_t _User_size = _Count * _Sz;

 #if defined(_M_IX86) || defined(_M_X64)
	if (_Try_aligned_allocation
		&& _BIG_ALLOCATION_THRESHOLD <= _User_size)
		{	// allocate large block
		static_assert(sizeof (void *) < _BIG_ALLOCATION_ALIGNMENT,
			"Big allocations should at least match vector register size");
		const size_t _Block_size = _NON_USER_SIZE + _User_size;
		if (_Block_size <= _User_size)
			_Xbad_alloc();	// report no memory
		const uintptr_t _Ptr_container =
			reinterpret_cast<uintptr_t>(::operator new(_Block_size));
		_SCL_SECURE_ALWAYS_VALIDATE(_Ptr_container != 0);
		_Ptr = reinterpret_cast<void *>((_Ptr_container + _NON_USER_SIZE)
			& ~(_BIG_ALLOCATION_ALIGNMENT - 1));
		static_cast<uintptr_t *>(_Ptr)[-1] = _Ptr_container;

 #ifdef _DEBUG
		static_cast<uintptr_t *>(_Ptr)[-2] = _BIG_ALLOCATION_SENTINEL;
 #endif /* _DEBUG */
		}
	else
 #endif /* defined(_M_IX86) || defined(_M_X64) */

		{	// allocate normal block
		_Ptr = ::operator new(_User_size);
		_SCL_SECURE_ALWAYS_VALIDATE(_Ptr != 0);
		}
	return (_Ptr);
	}

		// FUNCTION _Deallocate
inline
	void _Deallocate(void * _Ptr, size_t _Count, size_t _Sz)
	{	// deallocate storage for _Count elements of size _Sz
 #if defined(_M_IX86) || defined(_M_X64)
	_SCL_SECURE_ALWAYS_VALIDATE(_Count <= (size_t)(-1) / _Sz);
	const size_t _User_size = _Count * _Sz;
	if (_BIG_ALLOCATION_THRESHOLD <= _User_size)
		{	// deallocate large block
		const uintptr_t _Ptr_user = reinterpret_cast<uintptr_t>(_Ptr);
		_SCL_SECURE_ALWAYS_VALIDATE(
			(_Ptr_user & (_BIG_ALLOCATION_ALIGNMENT - 1)) == 0);
		const uintptr_t _Ptr_ptr = _Ptr_user - sizeof(void *);
		const uintptr_t _Ptr_container =
			*reinterpret_cast<uintptr_t *>(_Ptr_ptr);

 #ifdef _DEBUG
		// If the following asserts, it likely means that we are performing
		// an aligned delete on memory coming from an unaligned allocation.
		_SCL_SECURE_ALWAYS_VALIDATE(
			reinterpret_cast<uintptr_t *>(_Ptr_ptr)[-1] ==
				_BIG_ALLOCATION_SENTINEL);
 #endif /* _DEBUG */

		// Extra paranoia on aligned allocation/deallocation
		_SCL_SECURE_ALWAYS_VALIDATE(_Ptr_container < _Ptr_user);

 #ifdef _DEBUG
		_SCL_SECURE_ALWAYS_VALIDATE(2 * sizeof(void *)
			<= _Ptr_user - _Ptr_container);

 #else /* _DEBUG */
		_SCL_SECURE_ALWAYS_VALIDATE(sizeof(void *)
			<= _Ptr_user - _Ptr_container);
 #endif /* _DEBUG */

		_SCL_SECURE_ALWAYS_VALIDATE(_Ptr_user - _Ptr_container
			<= _NON_USER_SIZE);

		_Ptr = reinterpret_cast<void *>(_Ptr_container);
		}
 #endif /* defined(_M_IX86) || defined(_M_X64) */

	::operator delete(_Ptr);
	}
上面的程式碼有很多typedef和巨集,還有一些判斷和assert,看起來比較複雜,下面是精簡之後的實現(只保留比較關鍵的部分)

void *_Allocate(size_t _Count, size_t _Sz, bool _Try_aligned_allocation = true)
{	// allocate storage for _Count elements of size _Sz
    void *_Ptr = 0;

    // 計算需要記憶體的位元組數
    const size_t _User_size = _Count * _Sz;

// _BIG_ALLOCATION_THRESHOLD 為4096 大於這個大小的記憶體塊需要對齊。
//1、為什麼以4096為界?
    if (_Try_aligned_allocation && _BIG_ALLOCATION_THRESHOLD <= _User_size)
    {	// 分配大記憶體塊
        // _BIG_ALLOCATION_ALIGNMENT 大記憶體對齊 32
        // _NON_USER_SIZE 為 (2 * sizeof(void *) + _BIG_ALLOCATION_ALIGNMENT - 1) 即兩個指標大小再加31
        const size_t _Block_size = _NON_USER_SIZE + _User_size;
        // 這裡將地址轉換成整型是為了進行位運算,uintptr_t可能是unsigned int(32位),或者是unsigned long long(64位)
        const uintptr_t _Ptr_container =
            reinterpret_cast<uintptr_t>(::operator new(_Block_size));
        // 獲取對齊地址,低5位清零。
        // 2、為什麼是按32個位元組對齊?
        _Ptr = reinterpret_cast<void *>((_Ptr_container + _NON_USER_SIZE)
            & ~(_BIG_ALLOCATION_ALIGNMENT - 1));
        // 用_NON_USER_SIZE中的位置存放真正的記憶體塊起始地址
        static_cast<uintptr_t *>(_Ptr)[-1] = _Ptr_container;
    }
    else
    {	// 分配一般記憶體塊
        _Ptr = ::operator new(_User_size);
    }
    return (_Ptr);
}

void _Deallocate(void * _Ptr, size_t _Count, size_t _Sz)
{	// deallocate storage for _Count elements of size _Sz
    const size_t _User_size = _Count * _Sz;
    if (_BIG_ALLOCATION_THRESHOLD <= _User_size)
    {	// 釋放大記憶體塊
        // 將地址轉換為整數型別,以便做減法運算
        const uintptr_t _Ptr_user = reinterpret_cast<uintptr_t>(_Ptr);
        const uintptr_t _Ptr_ptr = _Ptr_user - sizeof(void *);
        // 獲取_NON_USER_SIZE中存放的真正的記憶體塊起始地址
        const uintptr_t _Ptr_container =
            *reinterpret_cast<uintptr_t *>(_Ptr_ptr);
        _Ptr = reinterpret_cast<void *>(_Ptr_container);
    }

    // 真正釋放記憶體
    ::operator delete(_Ptr);
}

問題解釋

1、

2、

5. 實現三:STLport中的實現

實現一太簡單,只是對operator new和operator delete做了簡單的封裝;實現二也比較簡單,微軟似乎把記憶體分配策略實現在底層。如果需要了解比較細膩記憶體分配策略(記憶體池),參考STLport中的實現。

STLport中的實現分析參見:《STL原始碼剖析》—侯捷 2.2.6-2.2.10

6. 實現四:帶記憶體池的實現

6.1 記憶體池

按照《STL原始碼剖析》—侯捷 2.2.6-2.2.10的思路,下面自己實現一個記憶體池,不考慮執行緒安全。

記憶體池分配記憶體,當所需的記憶體大小大於128時直接呼叫operator new分配記憶體,否則從空閒連結串列中分配,這樣就避免太多小記憶體塊造成的記憶體碎片和管理記憶體的額外負擔造成記憶體利用率不高的問題。

首先是管理記憶體池的資料結構,記憶體池維護16個空閒連結串列,為了方便管理(分配和回收),各自管理的記憶體塊大小都是8的倍數,分別為8,16,24,…,128。分配記憶體時,如果大小不是8的倍數,則將需求上調至8的倍數,然後從相應的空閒連結串列中分配。為了維護空閒連結串列中的連結串列結構,空閒塊的前sizeof(void *)個位元組儲存下一個空閒塊節點的地址,當空閒塊被分配後,空閒塊也將從空閒連結串列中摘除,所以這種做法不影響使用者使用,同時不需要額外記憶體作為節點的next指標。

class MemoryPool
{
private:
    // 小型記憶體塊的大小都是ms_align的倍數
    constexpr static size_t ms_align = 8;
    // 小型記憶體塊大小的上限
    constexpr static size_t ms_maxBytes = 128;
    // 空閒記憶體塊連結串列的個數
    constexpr static size_t ms_nFreeLists = ms_maxBytes / ms_align;
private:
    // 記憶體塊空閒連結串列,m_freeLists[0]是大小為8的記憶體塊連結串列,m_freeLists[1]是大小為16的...
    void *m_freeLists[ms_nFreeLists];
private:
    // 記憶體池分配塊
    void *m_pFreeStart;
    void *m_pFreeEnd;
};
完整的記憶體池定義和實現:

class MemoryPool
{
public:
    MemoryPool() :m_freeLists{ nullptr }, m_pFreeStart(nullptr), m_pFreeEnd(nullptr), m_heapSize(0) {}
    // 分配記憶體
    void *allocate(size_t nBytes);
    // 回收記憶體
    void deallocate(void *ptr, size_t nBytes);
private:
    // 小型記憶體塊的大小都是ms_align的倍數
    constexpr static size_t ms_align = 8;
    // 小型記憶體塊大小的上限
    constexpr static size_t ms_maxBytes = 128;
    // 空閒記憶體塊連結串列的個數
    constexpr static size_t ms_nFreeLists = ms_maxBytes / ms_align;
private:
    // 記憶體塊空閒連結串列,m_freeLists[0]是大小為8的記憶體塊連結串列,m_freeLists[1]是大小為16的...
    void *m_freeLists[ms_nFreeLists];
private:
    // 記憶體池
    char *m_pFreeStart;
    char *m_pFreeEnd;
    // 本以為m_heapSize沒有用,原來為記憶體池分配記憶體時可以作為計算增量的因子
    size_t m_heapSize;
private:
    // 將bytes上調至ms_align的倍數
    size_t roundUp(size_t nBytes)
    {
        return (nBytes + ms_align - 1)&~(ms_align - 1);
    }

    // 根據記憶體塊的大小求得應使用第幾個freeList,從0開始
    size_t freeListIndex(size_t nBytes)
    {
        return (nBytes + ms_align - 1) / ms_align - 1;
    }
private:
    // 重新為nBytes所屬的空閒連結串列分配空閒節點,nBytes是ms_align的倍數
    void *refill(size_t nBytes);
    // 從記憶體池獲取記憶體,nBytes是ms_align的倍數
    void *chunkAlloc(size_t nBytes, int &nObjs);
};

void * MemoryPool::allocate(size_t nBytes)
{
    if (nBytes > ms_maxBytes)
    {   // 當記憶體塊較大,直接從系統分配
        return ::operator new(nBytes);
    }

    // 獲取nBytes大小所屬的空閒連結串列
    void **pFreeList = m_freeLists + freeListIndex(nBytes);
    void *result = *pFreeList;

    if (result == nullptr)
    {   // 空閒連結串列中沒有空閒塊,需要重新分配
        return refill(roundUp(nBytes));
    }

    // 有空閒塊,將該空閒塊從空閒連結串列中摘除
    *pFreeList = *reinterpret_cast<void **>(result);

    return result;
}

// 從這個函式可以看出回收記憶體時並不會將記憶體交還系統,空閒連結串列的記憶體只增不減
// 考慮到分配的記憶體都是碎片級別的,非極端情況下閒佔的記憶體不會太多,所以不考慮將記憶體交還系統
void MemoryPool::deallocate(void *ptr, size_t nBytes)
{
    if (nBytes > ms_maxBytes)
    {   // 和allocate對應,直接給系統回收
        ::operator delete(ptr);
    }

    // 獲取nBytes大小所屬的空閒連結串列
    void **pFreeList = m_freeLists + freeListIndex(nBytes);

    // 將記憶體塊重新加入空閒連結串列
    *reinterpret_cast<void **>(ptr) = *pFreeList;
    *pFreeList = ptr;
}

void * MemoryPool::refill(size_t nBytes)
{
    int nObjs = 20;
    // 嘗試從記憶體池獲取nObjs個記憶體塊,可能結果小於nObjs個
    void *pChunk = chunkAlloc(nBytes, nObjs);

    // 不止一塊,除了第一塊之外都加入空閒連結串列
    if (nObjs > 1)
    {
        // 獲取nBytes大小所屬的空閒連結串列
        void **pFreeList = m_freeLists + freeListIndex(nBytes);

        void *pNext = reinterpret_cast<char *>(pChunk) + nBytes;
        // 將第二塊連線到表頭
        *pFreeList = pNext;
        for (int i = 2;i < nObjs;++i)
        {
            void **pCurrent = reinterpret_cast<void **>(pNext);
            pNext = reinterpret_cast<char *>(pChunk) + nBytes;
            // 將pNext連線到空閒連結串列
            *pCurrent = pNext;
        }
        // 空閒連結串列最後一個next指標為空
        *reinterpret_cast<void **>(pNext) = nullptr;
    }
    // 返回第一塊給使用者
    return pChunk;
}

void * MemoryPool::chunkAlloc(size_t nBytes, int &nObjs)
{
    size_t nNeedBytes = nBytes * nObjs;
    size_t nFreeBytes = m_pFreeEnd - m_pFreeStart;
    void *result;
    if (nNeedBytes <= nFreeBytes)
    {   // 記憶體池中還有足夠的記憶體
        result = m_pFreeStart;
        m_pFreeStart += nNeedBytes;
    }
    else if (nBytes <= nFreeBytes)
    {   // 記憶體池記憶體還夠一個或以上的記憶體塊
        nObjs = nFreeBytes / nBytes;
        result = m_pFreeStart;
        m_pFreeStart += nBytes * nObjs;
    }
    else
    {   // 記憶體池記憶體連一個記憶體塊也不夠了
        if (nFreeBytes > 0)
        {   // 記憶體池還有零頭,將零頭加入合適的空閒連結串列
            // 從這裡可以看出將記憶體塊的上調至ms_align的倍數這個設計真是太精巧了
            void **pFreeList = m_freeLists + freeListIndex(nFreeBytes);
            *reinterpret_cast<void **>(m_pFreeStart) = *pFreeList;
            *pFreeList = *pFreeList;
            // 當下面operator new記憶體分配失敗時,m_pFreeStart狀態保證合法
            m_pFreeStart += nFreeBytes;
        }

        // 向系統申請的記憶體大小,2倍所需再加上附加增量
        // STLport中是這樣計算的,可能這樣會比較高效
        size_t nBytesToGet = 2 * nNeedBytes + roundUp(m_heapSize >> 4);
        m_pFreeStart = reinterpret_cast<char *>(operator new(nBytesToGet));
        m_heapSize += nBytesToGet;
        m_pFreeEnd = m_pFreeStart + nBytesToGet;
        // 遞迴呼叫,非常可以的做法
        return chunkAlloc(nBytes, nObjs);
    }
    return result;
}

6.2 帶記憶體池的allocator實現

這裡和實現一相同的部分省略,僅給出分配和釋放記憶體的幾個操作:

template<class T>
class allocator
{
private:
    static MemoryPool pool;
public:
…… // 同實現一
    pointer allocate(size_type count, allocator<void>::const_pointer hint = nullptr)
    {
        return static_cast<pointer>(pool.allocate(count * sizeof(value_type)));
    }

    // 釋放記憶體
    void deallocate(pointer ptr, size_type count)
    {
        pool.deallocate(ptr, count);
    }
…… // 同實現一
};