leveldb原始碼分析(1)--arena記憶體池的實現

Jefffrey發表於2019-05-13

引言

陳碩老師曾經說過大部分情況下自己實現一個記憶體池是沒有必要的,我們要相信malloc的作者。但是leveldb的作者也不是凡夫俗子(去google jeff dean 網 上 有 真 相)。leveldb為什麼自己實現一個記憶體池我相信一定有它的道理,雖然這篇文章不大可能道出作者的精妙想法,但還是試著解釋清楚程式的基本思路和框架。

基本思想

為什麼不用new或者malloc分配記憶體?

  • 頻繁地分配記憶體,造成記憶體碎片化
  • new完忘記delete,造成記憶體洩漏

leveldb的做法:先向系統申請一塊大的記憶體,外部需要申請記憶體時,先把已有的記憶體塊分配給使用者,如果不夠用則再申請一塊大的記憶體。當記憶體池物件析構時,分配的記憶體均被釋放,保證了記憶體不會洩漏。

原始碼分析

/util/arena.h/util/arena.cc 包含了記憶體池的定義和實現。以下是Arena類的宣告。

宣告

public:
  Arena();//建構函式
  ~Arena();//解構函式
  char* Allocate(size_t bytes);//分配"bytes"大小的空間並返回指向該地址的指標
  char* AllocateAligned(size_t bytes);//分配“byte”大小的空間,保證字對齊。
  size_t MemoryUsage() const//返回已分配記憶體的大小(不精確)
private:
  char* AllocateFallback(size_t bytes);//當前記憶體塊的剩餘容量不足以支撐所申請記憶體時,呼叫此函式
  char* AllocateNewBlock(size_t block_bytes);//使用malloc分配一個新的記憶體塊

  // Allocation state
  char* alloc_ptr_;//指向當前記憶體塊剩餘空間的地址
  size_t alloc_bytes_remaining_;//當前記憶體塊剩餘容量

  std::vector<char*> blocks_;//存放指向記憶體塊地址的指標,用於解構函式中釋放記憶體

  size_t blocks_memory_;//已分配記憶體大小

  Arena(const Arena&);//只申明不實現,防止編譯器隱式拷貝,詳情可自行查詢google c++ style
  void operator=(const Arena&);//同上

實現

char* Arena::AllocateFallback(size_t bytes) {
  if (bytes > kBlockSize / 4) {
    char* result = AllocateNewBlock(bytes);
    return result;
  }
  alloc_ptr_ = AllocateNewBlock(kBlockSize);
  alloc_bytes_remaining_ = kBlockSize;

  char* result = alloc_ptr_;
  alloc_ptr_ += bytes;
  alloc_bytes_remaining_ -= bytes;
  return result;

如果申請記憶體大於塊大小的1/4,我們將單獨為其申請一塊大小正好的新記憶體塊,防止記憶體塊的其他空間被浪費。否則我們將申請固定大小的記憶體塊,並修改狀態。

char* Arena::AllocateAligned(size_t bytes) {
  const int align = (sizeof(void*) > 8) ? sizeof(void*) : 8;
//指標大小,由編譯器產生的目標平臺的指令集決定。譬如說x86就是4,x64就是8 by vczh
  assert((align & (align-1)) == 0);   //確保指標大小為2指數倍
  size_t current_mod = reinterpret_cast<uintptr_t>(alloc_ptr_) & (align-1);//強制型別轉換
  size_t slop = (current_mod == 0 ? 0 : align - current_mod);
//可以看作是記憶體的空隙
  size_t needed = bytes + slop;//填滿空隙後所需記憶體大小
  char* result;
  if (needed <= alloc_bytes_remaining_) {
    result = alloc_ptr_ + slop;//從對齊處開始分配記憶體
    alloc_ptr_ += needed;
    alloc_bytes_remaining_ -= needed;
  } else {
    //不需要再添空隙,因為該函式永遠對齊
    result = AllocateFallback(bytes);
  }
  assert((reinterpret_cast<uintptr_t>(result) & (align-1)) == 0);
  return result;
}

reinterpret_cast為暴力轉換,請慎用。
最後看一下MemoryUsage的實現,

size_t MemoryUsage() const {
    return blocks_memory_ + blocks_.capacity() * sizeof(char*);
  }

返回已分配記憶體塊的大小以及指向記憶體塊指標的大小,忽略了Arena其他資料成員的大小,如alloc_ptr_,alloc_bytes_remaining_ 等。

總結

不推薦在實際工程中自己實現一個記憶體池,因為內建的malloc在一般情況下的效能是非常高的,自己實現的記憶體池不能保證bugfree並且效能也不一定比內建的malloc強,leveldb的arena實現僅供學習和參考。我猜想由於leveldb需要特殊的字對齊記憶體分配功能,所以作者用arena實現了定製的記憶體池。
To be continued

@See You SpaceCowboy

相關文章