loki 是書籍 《Modern C++ Design》配套發行的一個 C++ 代碼庫,裏面對模板的使用發揮到了極致,對設計模式進行了代碼實現。這裏是 loki 庫的源碼。
ps. 有空是不是應該把裏面的設計模式的代碼學習學習, hahahah
loki 庫裏面有兩個文件,SmallObj.h 以及 SmallObj.cpp,就是一個內存管理的內,可以單獨使用。下面就其源碼進行分析。
1. 類層次結構
SmallObj 文件裏面有三個類:Chunk
,FixedAllocator
和 SmallObjAllocator
。它們的類層次關係如下,其中SmallObjAllocator
是最上層的,直接供客戶端使用的類。
2. Chunk
Chunk
就是直接管理單一內存塊的類。它負責向操作系統索取內存塊,並將內存塊串成 “鏈表”。
2.1 初始化
先來看看其中的初始化函數 Init()
:
void FixedAllocator::Chunk::Init(std::size_t blockSize, unsigned char blocks)
{
pData_ = new unsigned char[blockSize * blocks];
Reset(blockSize, blocks);
}
void FixedAllocator::Chunk::Reset(std::size_t blockSize, unsigned char blocks)
{
firstAvailableBlock_ = 0;
blocksAvailable_ = blocks;
unsigned char i = 0;
unsigned char* p = pData_;
for (; i != blocks; p += blockSize) //指向下一個可用的 block 的 index
{
*p = ++i;
}
}
- 傳入參數爲 block 的大小和數量
- 用 operator new 分配出一大塊內存 chunk,並用指針
pData_
指向 chunk。 Reset()
函數對這塊內存進行分割。利用嵌入式指針的思想,每一塊 block 的第一個 byte 存放的是下一個可用的 block 距離起始位置 pData_ 的偏移量(以 block 大小爲單位),以這種形式將 block 串成 “鏈表”。firstAvailableBlock_
表示當前可用的 block 的偏移量;blocksAvailable_
表示當前 chunk 中剩餘的 block 數量。
初始狀態的 chunk 如下圖所示:
2.2 內存分配 allocate
void* FixedAllocator::Chunk::Allocate(std::size_t blockSize)
{
if (!blocksAvailable_) return 0;
unsigned char* pResult =
pData_ + (firstAvailableBlock_ * blockSize);
firstAvailableBlock_ = *pResult;
--blocksAvailable_;
return pResult;
}
這段代碼也很好理解,可以結合下圖理解:
2.3 內存回收 deallocate
void FixedAllocator::Chunk::Deallocate(void* p, std::size_t blockSize)
{
unsigned char* toRelease = static_cast<unsigned char*>(p);
//鏈接到鏈表上
*toRelease = firstAvailableBlock_;
//修改 “頭指針”
//回收的 block 指針距離頭指針 pData_ 的距離(以 block 爲單位)
firstAvailableBlock_ = static_cast<unsigned char>(
(toRelease - pData_) / blockSize);
++blocksAvailable_;
}
同之前的內存回收一樣,回收的時候,都把 block 插到鏈表的頭部。
3. FixedAllocate
FixedAllocate 則負責管理一個具有相同 block size 的 chunk 的集合。它負責根據客戶需求,創建特定 block 大小的 chunk ,並放置在容器 vector 中進行管理。
3.1 內存分配 allocate
void* FixedAllocator::Allocate()
{
if (allocChunk_ == 0 || allocChunk_->blocksAvailable_ == 0)
{ //目前沒有標定的 chunk ,或者這個 chunk 已經用完了
//從頭找起
Chunks::iterator i = chunks_.begin();
for (;; ++i)
{
//沒找到,則創建一個新的 chunk
if (i == chunks_.end())
{
// Initialize
chunks_.reserve(chunks_.size() + 1);
Chunk newChunk;
newChunk.Init(blockSize_, numBlocks_);
chunks_.push_back(newChunk);
allocChunk_ = &chunks_.back();
//上面容器的大小會增長一個,可能會引起 vetor 的擴容操作,導致原來的元素被搬移到新的地方
//所以這裏的 deallocChunk 需要重新標定,直接標定爲第一個。原來可能並不是指向第一個,
//原來的那個經過搬移之後已經無法確定新的位置了
deallocChunk_ = &chunks_.front();
break;
}
if (i->blocksAvailable_ > 0)
{
allocChunk_ = &*i;
break;
}
}
}
return allocChunk_->Allocate(blockSize_);
}
allocChunk_
指向當前正在使用中的 chunk。如果allocChunk_
指向的 chunk 中的 block 已經用完了,那麼就在容器中去尋找其他可用的 chunk 。如果沒有找到,就新建一個 chunk ,放進容器中,並標定爲當前的allocChunk_
。- 本來這裏的 allocate 動作跟
deallocChunk_
成員沒有關係的。但是,創建新的 chunk 並添加進 vector 中後,可能會引起 vector 的內存重分配動作,導致原來的deallocChunk_
指向的內存並不存在了,所以要對deallocChunk_
重新標定。它這裏直接重新標定爲第一個 chunk,因爲原來的那個已經無法確定位置了。 ps. 大神就是大神,這都能想到~
其實我這裏有一個疑問。用 allocChunk_ 變量對當前使用的 chunk 進行標定,如果當前使用的 chunk 沒有用完,會一直使用這一塊 chunk,直到這一塊 chunk 用完。那麼,當這一塊 chunk 用完的時候,其他的 chunk 難道不是能夠確定一定用完了嗎?那麼還有必要去對 vector 進行遍歷,去尋找可用的 chunk 嗎?爲什麼不直接就去創建一個新的 chunk ?
3.2 內存回收 deallocate
我們需要根據歸還內存的位置,把這塊內存回收到相對應的 chunk 中。
void FixedAllocator::Deallocate(void* p)
{
deallocChunk_ = VicinityFind(p);
DoDeallocate(p);
}
- 我們知道每一塊 chunk 的頭指針,以及這一塊 chunk 的大小,這樣的話,我們就可以計算出每一塊 chunk 的地址範圍。我們只要找到歸還的內存的地址是落在哪一個 chunk 地址範圍內,就可以確定 chunk。這一功能由函數
VicinityFind()
實現。 VicinityFind()
函數採用一種分頭查找的算法,從上一次 deallocChunk_ 的位置出發,在容器中分兩頭查找。這也應該是設計這個 deallocChunk_ 指針的原因把。內存分配通常是給容器服務的。而容器內元素連續創建時,通常就從同一個 chunk 獲得連續的地址空間。歸還的時候當然也需要歸還到同一塊 chunk 。通過對上一次歸還 chunk 的記錄,能提高搜索的效率。下面是 VicinityFind() 的實現代碼:
FixedAllocator::Chunk* FixedAllocator::VicinityFind(void* p)
{
const std::size_t chunkLength = numBlocks_ * blockSize_;
Chunk* lo = deallocChunk_;
Chunk* hi = deallocChunk_ + 1;
Chunk* loBound = &chunks_.front();
Chunk* hiBound = &chunks_.back() + 1;
// Special case: deallocChunk_ is the last in the array
if (hi == hiBound) hi = 0;
for (;;)
{
if (lo)
{
if (p >= lo->pData_ && p < lo->pData_ + chunkLength)
{
return lo;
}
if (lo == loBound) lo = 0;
else --lo;
}
if (hi)
{
if (p >= hi->pData_ && p < hi->pData_ + chunkLength)
{
return hi;
}
if (++hi == hiBound) hi = 0;
}
}
}
- 最後內存回收的動作由函數
DoDeallocate()
完成。如果當前回收的 chunk 已經將所有的 block 全部回收完了,即deallocChunk_->blocksAvailable_ == numBlocks_
,本來這塊內存就可以歸還給 OS 了的。但是這裏採取了一個延遲歸還的動作。把這個空的 chunk 通過swap
函數放在 vector 的末尾,並且將allocChunk_
指向它,供下一次再使用。只有當有兩個空 chunk 出現時,纔會把上一個空的 chunk 歸還給 OS。下面是源碼:
void FixedAllocator::DoDeallocate(void* p)
{
// call into the chunk, will adjust the inner list but won't release memory
deallocChunk_->Deallocate(p, blockSize_);
//如果已經全回收了
if (deallocChunk_->blocksAvailable_ == numBlocks_)
{
// deallocChunk_ is completely free, should we release it?
Chunk& lastChunk = chunks_.back();
//最後一個就是當前的 deallocChunk
if (&lastChunk == deallocChunk_)
{
// check if we have two last chunks empty
if (chunks_.size() > 1 &&
deallocChunk_[-1].blocksAvailable_ == numBlocks_)
{
// Two free chunks, discard the last one
lastChunk.Release();
chunks_.pop_back();
allocChunk_ = deallocChunk_ = &chunks_.front();
}
return;
}
if (lastChunk.blocksAvailable_ == numBlocks_)
{
// Two free blocks, discard one
lastChunk.Release();
chunks_.pop_back();
allocChunk_ = deallocChunk_;
}
else
{
// move the empty chunk to the end
std::swap(*deallocChunk_, lastChunk);
allocChunk_ = &chunks_.back();
}
}
}
這裏我突然明白上面說到的,爲什麼要遍歷容器尋找下一個可用的 chunk 的問題了。因爲在這裏,allocChunk_ 會轉而指向這個回收完成了的空的 chunk,它原來指向的 chunk 可能並沒有使用完。
不過這裏我又有一個新的疑惑。如果當前空的 chunk 就是容器中最後一個時,爲什麼要往前看一個 chunk,看它是不是空?前面一個有可能是空嗎??
4. SmallObjAllocator
SmallObjAllocator 則負責管理具有不同 block size 的 FixedAllocate 的 vector 集合。
4.1 內存分配 allocate
void* SmallObjAllocator::Allocate(std::size_t numBytes)
{
if (numBytes > maxObjectSize_) return operator new(numBytes);
if (pLastAlloc_ && pLastAlloc_->BlockSize() == numBytes)
{
return pLastAlloc_->Allocate();
}
//找到第一個 >= numBytes 的位置
Pool::iterator i = std::lower_bound(pool_.begin(), pool_.end(), numBytes);
//沒找到相同的,就重新創建一個 FixedAllocator
if (i == pool_.end() || i->BlockSize() != numBytes)
{
i = pool_.insert(i, FixedAllocator(numBytes));
pLastDealloc_ = &*pool_.begin();
}
pLastAlloc_ = &*i;
return pLastAlloc_->Allocate();
}
SmallObjAllocator
不可能無窮無盡的滿足客戶不同的 block size 的需求。它設有一個最大的 block size 變量maxObjectSize_
。如果客戶端需求的 block size 大於這個 threshold,就直接交由 operator new 去進行處理。pLastAlloc_
記錄上一次分配 block 的 FixedAllocator object 。如果這一次需求的 block size 等於上一次分配的 block size,就直接使用同一個 FixedAllocator object 去分配內存。我認爲這個變量的設計和 FixedAllocator 中deallocChunk_
的設計道理是一樣的。 SmallObjAllocator 是給容器服務,而容器通常連續多次爲其中的 element 索取多個相同 size 的 block,所以對上一次分配的 FixedAllocator object 進行記錄能夠減少不必要的查找動作。- 如果這一次需求的 block size 不等於上一次分配的 block size,就遍歷容器尋找不小於需求的 block size 而且最接近的位置,也就是
std::lower_bound()
函數的功能。如果找到 block size 相等的,就直接分配;如果沒找到相等的,就在該位置上插入一個新的 FixedAllocator object。同樣,爲了防止 vector 擴容操作引起重新分配內存,需要對pLastDealloc_
進行重定位。
4.2 內存回收 deallocate
void SmallObjAllocator::Deallocate(void* p, std::size_t numBytes)
{
if (numBytes > maxObjectSize_) return operator delete(p);
if (pLastDealloc_ && pLastDealloc_->BlockSize() == numBytes)
{
pLastDealloc_->Deallocate(p);
return;
}
Pool::iterator i = std::lower_bound(pool_.begin(), pool_.end(), numBytes);
assert(i != pool_.end());
assert(i->BlockSize() == numBytes);
pLastDealloc_ = &*i;
pLastDealloc_->Deallocate(p);
}
- 設計上沒啥新穎的,不多說了。
5. 總結
相比於之前分析的 std::alloc
的內存管理:
std::alloc
一旦向 OS 索取了新的 chunk,就不會還給 OS 了,一直在自己的掌控之中。因爲它裏面的指針拉扯比較複雜,幾乎不可能去判斷一塊 chunk 中給出去的 block 是否全部歸還了。但是loki::allocator
通過利用一個blocksAvailable_
變量,就很容易的判斷出某一塊 chunk 中的 block 是否已經全部歸還了,這樣就可以歸還給 OS。std::alloc
只負責一些特定 block size 的內存管理。如果客戶端需要的 block size 它並不支持,那個客戶端的 block size 會被取整到最接近的大小 (當然前提是小於它所能夠分配的最大的 block size);但是loki::allocator
能夠爲不大於最大 block size 的所有 block size 服務。