【C++內存管理】loki::allocator 源碼分析

loki 是書籍 《Modern C++ Design》配套發行的一個 C++ 代碼庫,裏面對模板的使用發揮到了極致,對設計模式進行了代碼實現。這裏是 loki 庫的源碼。

ps. 有空是不是應該把裏面的設計模式的代碼學習學習, hahahah

loki 庫裏面有兩個文件,SmallObj.h 以及 SmallObj.cpp,就是一個內存管理的內,可以單獨使用。下面就其源碼進行分析。

1. 類層次結構


SmallObj 文件裏面有三個類:ChunkFixedAllocatorSmallObjAllocator。它們的類層次關係如下,其中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 服務。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章