簡介
主要是對開源代碼的學(xué)習(xí)。按自己的理解把代碼重寫了一遍诵姜,添加了中文注釋搏熄。下面放鏈接。
原理
一般而言宵凌,我們使用 malloc
或者 new
來動態(tài)管理內(nèi)存瞎惫。 然而译株,這些函數(shù)由于牽涉到系統(tǒng)調(diào)用,效率很低乘寒。于是伞辛,如果我們使用很少的系統(tǒng)調(diào)用夯缺,申請一大塊內(nèi)存自己管理,就非常好了瞧捌。這就是內(nèi)存池的思想。內(nèi)存池向系統(tǒng)申請大塊內(nèi)存殿怜,然后再分為小片給程序使用头谜。每次我們請求內(nèi)存鸠澈,其實是在內(nèi)存池里拿而非通過系統(tǒng)調(diào)用。它的優(yōu)勢體現(xiàn)在:
- 它從原理上比
malloc
以及new
快 - 分配內(nèi)存時不存在 overhead
- 幾乎不存在內(nèi)存碎片
- 無須一個一個釋放對象际度,只需要釋放內(nèi)存池即可
內(nèi)存池也有缺陷:
- 需要知道對象的大小
- 需要根據(jù)應(yīng)用的情況來調(diào)節(jié)
源碼閱讀筆記
= delete 用法
/*******
* 賦值 *
*******/
// 使用 = delete 禁用拷貝賦值乖菱,只保留移動賦值
MemoryPool& operator=(const MemoryPool& mp) = delete;
MemoryPool& operator=(const MemoryPool&& mp) noexcept;
參考 stackoverflow 上的相關(guān)問題蓬网。一句話總結(jié)就是禁用函數(shù)帆锋。
以上代碼實際上就是禁用拷貝賦值,僅使用移動賦值皮官。這是顯然的哲鸳,內(nèi)存池拷貝代價太大。
static_assert 用法
// static_assert: 編譯期斷言檢查
static_assert(BlockSize >= 2 * sizeof(Slot_), "BlockSize too small.")
在編譯期間執(zhí)行檢查讯沈,如果不通過條件 arg1缺狠,則報錯 arg2萍摊。
由于是編譯期間運行,因此只能用于編譯期間就確定了的常量穷劈。尤其常用于模板類的檢查。
使用 union 的理由
union Slot_ {
value_type element;
Slot_* next;
};
一個奇怪的構(gòu)造函數(shù)
// MemoryPool.h
MemoryPool(const MemoryPool& mp) noexcept; // 拷貝構(gòu)造函數(shù)
// MemoryPool.cpp
template <typename T, size_t BlockSize>
MemoryPool<T, BlockSize>::MemoryPool(const MemoryPool& mp) noexcept:
MemoryPool() {
}
推測其意思就是調(diào)用無參數(shù)構(gòu)造函數(shù)社证。
析構(gòu)函數(shù)中的強制類型轉(zhuǎn)換
template <typename T, size_t BlockSize>
MemoryPool<T, BlockSize>::~MemoryPool()
noexcept
{
slot_pointer_ curr = currentBlock_;
while (curr != nullptr) {
slot_pointer_ prev = curr->next;
operator delete(reinterpret_cast<void*>(curr));
curr = prev;
}
}
比較費解為什么要強制轉(zhuǎn)為 void* 指針追葡。源碼中多次使用了 reinterpret_cast
奕短,具體可以參考 static_cast 和 reinterpret_cast 。
內(nèi)存池實現(xiàn)細(xì)節(jié)
直接上圖谬返,這個圖說明了內(nèi)存池的運作原理:
首先定義了一個 union朱浴,非常值得注意的是达椰,這不是一個鏈表啰劲。這個 union 的本質(zhì)是復(fù)用檀何,一個 slot 里,既可能存的是數(shù)據(jù)栓辜,也可能存的是指向另一個 slot 的指針藕甩。這樣的設(shè)計有助于節(jié)省空間周荐,在某個對象不再需要時,直接放入 freeSlots_ 鏈表中腋妙,并改寫 next 指針讯榕。
union Slot_ {
T element;
Slot_ *next;
};
為了實現(xiàn)內(nèi)存池匙睹,我們需要四個指針:
Slot_ *currentBlock_;
Slot_ *currentSlot_;
Slot_ *lastSlot_;
Slot_ *freeSlots_; // 這是一個比較讓人疑惑的名字垃僚,實際是指的 deallocate 之后空閑出來的 slots
結(jié)合上面圖中的位置谆棺,可以大概理解其運作方式罕袋。注意圖中沒有標(biāo)出 freeSlots_,因為這個指針如果沒有進(jìn)行 deallocate朵夏,就保持為 nullptr仰猖。
下面分析最關(guān)鍵的兩個函數(shù)奈籽。
- 內(nèi)存池向系統(tǒng)申請內(nèi)存
template <typename T, size_t BlockSize>
void MemoryPool<T, BlockSize>::allocateBlock() {
// 頭插鏈表
// A->B->C 插入 X
// X->A->B->C
char *newBlock = reinterpret_cast<char *>(::operator new(BlockSize));
reinterpret_cast<Slot_ *>(newBlock)->next = currentBlock_;
currentBlock_ = reinterpret_cast<Slot_ *>(newBlock);
// 這里需要作一個內(nèi)存對齊,因為第一個區(qū)域存放的是指向上一個 Block 的指針躏升,而不是 Slot
// 而之后的都是Slot
char *body = newBlock + sizeof(Slot_ *);
size_t bodyPadding = padPointer(body, alignof(Slot_));
currentSlot_ = reinterpret_cast<Slot_ *>(body + bodyPadding);
lastSlot_ =
reinterpret_cast<Slot_ *>(newBlock + BlockSize - sizeof(Slot_) + 1);
}
里面的注意點都用中文注釋出來了狼忱。
- 在內(nèi)存池中為對象分配內(nèi)存
inline T *allocate(size_t n = 1, const T *hint = nullptr) {
if (freeSlots_ != nullptr) {
// 存在 freeSlots
T *result = reinterpret_cast<T *>(freeSlots_);
freeSlots_ = freeSlots_->next;
return result;
} else {
// freeSlots 還未初始化或者已經(jīng)耗盡
if (currentSlot_ >= lastSlot_) {
allocateBlock();
}
return reinterpret_cast<T *>(currentSlot_++);
}
}
這里尤其需要注意的是钻弄,在本文的實現(xiàn)中,freeSlots_
只在釋放對象時才會被賦值饲帅。如果只進(jìn)行了分配沒有釋放批销,那么會一直執(zhí)行 else
中的代碼。
inline void deallocate(T *p, size_t = 1) {
if (p != nullptr) {
// 也是頭插鏈表
// 空閑鏈表 A->B->C丘逸,現(xiàn)在釋放了 X 的空間掀宋,需要將 X 加入空閑鏈表
// X->A->B->C
reinterpret_cast<Slot_ *>(p)->next = freeSlots_;
freeSlots_ = reinterpret_cast<Slot_ *>(p);
}
}
在釋放中仲锄,體現(xiàn)了 union 的作用儒喊。傳入的指針本來指向一個對象币呵,將其強制轉(zhuǎn)為 union Slot,再將 next 指針賦值為空閑鏈表頭芯义。此時就覆蓋對象的前 8 字節(jié)內(nèi)容(64 位機(jī)器)妻柒,對象失效。只留下前 8 字節(jié)指針被使用绑警。
內(nèi)存池的效果
運行測試函數(shù)可以得到:
Copyright (c) 2013 Cosku Acay, http://www.coskuacay.com
Provided to compare the default allocator to MemoryPool.
Default Allocator Time: 7.3624
MemoryPool Allocator Time: 1.23673
Here is a secret: the best way of implementing a stack is a dynamic array.
Vector Time: 2.14202
The vector implementation will probably be faster.
可以看出计盒,在 OS X 下,明顯快于原始的 allocator章郁。奇怪的是我在 Linux 操作系統(tǒng)下并沒有跑出這么大的差距,并且 vector 實現(xiàn)確實快于內(nèi)存池聊替。