1. 程式人生 > >leveldb 閱讀筆記(1) 內存分配

leveldb 閱讀筆記(1) 內存分配

erp ant 保存 系統調用 tin assert blog 方便 png

內存管理對於任何程序都是很重要的一塊,leveldb自己也實現了一個簡單了內存分配器,而不是使用一些其他開源軟件tcmalloc等,避免了對其他軟件的依賴。

自己實現內存分配器有什麽好處呢? 我認為主要有以下幾點:

1. 內存池的主要作用是減少new 、 delete 等的調用次數,也就是減少系統調用的開銷。

2. 減少內存碎片。

3. 方便內存使用統計和監控。

Arena 按塊申請內存,每塊大小為4k(這個值應該是和分頁大小相關),然後使用vector保存這些塊的首地址,模型如下:

技術分享

成員變量與publlic調用接口:

class Arena {
 public: 

  // 返回分配好的內存塊
char* Allocate(size_t bytes); // 返回分配好的內存塊,首地址滿足字節對齊 char* AllocateAligned(size_t bytes); // 已使用內存的估算大小(因為使用了stl的vector,精確大小不好確定) size_t MemoryUsage() const { return reinterpret_cast<uintptr_t>(memory_usage_.NoBarrier_Load()); } private: // Allocation state char* alloc_ptr_;                //
指向當前4k塊使用進度 size_t alloc_bytes_remaining_;        //當前4k塊剩余大小 // Array of new[] allocated memory blocks std::vector<char*> blocks_;          //記錄所申請各塊內存的首地址,方便回收 // Total memory usage of the arena. port::AtomicPointer memory_usage_;      //已經使用內存大小的估算 }

內存分配策略:

1. 當需剩余的內存大小滿足分配需求時,直接使用剩余的內存(之前一次性申請了一大塊,還有些沒用完)

否則需要向系統重新申請一塊。

2. 當前塊剩余的內存大小不滿足分配需求,並且需要分配的內存比較大時(>4096/4 = 1k),單獨申請一塊獨立的內存。

3. 當前塊剩余的內存不夠並且新的分配需求不大於1k時, 另外申請一大塊4k,從中取出部分返回給調用者,余下的供下次使用。

源碼的註釋中也說到了,上面第2點是為了避免過多的內存浪費,為什麽這麽做就能避免呢? 考慮一種情況:

假如當前塊還剩余1k大小,分配需求是 1025 bytes > 1k, 不按上面的做法的話,就需要新申請一個4k塊從中取出1025 bytes返回,然而這麽做的話,上一塊剩余的1k就再也不會被使用了,這就是浪費。 反而之前剩余的1k內存還可以繼續使用。

因此這種做法避免了大塊的浪費,然而仍有可能浪費1k之內的內存,為什麽不把這個值設的很小呢? 那就和直接使用new差不多了,失去了內存分配器的原有意義,設置成這個值是一個權衡利弊的結果。

具體實現:

inline char* Arena::Allocate(size_t bytes) {
  // The semantics of what to return are a bit messy if we allow
  // 0-byte allocations, so we disallow them here (we don‘t need
  // them for our internal use).
  assert(bytes > 0);
  if (bytes <= alloc_bytes_remaining_) {
    char* result = alloc_ptr_;
    alloc_ptr_ += bytes;
    alloc_bytes_remaining_ -= bytes;
    return result;
  }
  return AllocateFallback(bytes);
}

char* Arena::AllocateFallback(size_t bytes) {
  if (bytes > kBlockSize / 4) {
    // Object is more than a quarter of our block size.  Allocate it separately
    // to avoid wasting too much space in leftover bytes.
    char* result = AllocateNewBlock(bytes);
    return result;
  }

  // We waste the remaining space in the current block.
  alloc_ptr_ = AllocateNewBlock(kBlockSize);
  alloc_bytes_remaining_ = kBlockSize;

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

char* Arena::AllocateNewBlock(size_t block_bytes) {
  char* result = new char[block_bytes];
  blocks_.push_back(result);
  memory_usage_.NoBarrier_Store(
      reinterpret_cast<void*>(MemoryUsage() + block_bytes + sizeof(char*)));
  return result;
}

此外Arena 還提供了一個保證直接對齊的方法:

char* Arena::AllocateAligned(size_t bytes) {
  const int align = (sizeof(void*) > 8) ? sizeof(void*) : 8;  // 按一個指針所占大小的字節對其,最少為8
  assert((align & (align-1)) == 0);                  // 確保對其大小時2的冪
  size_t current_mod = reinterpret_cast<uintptr_t>(alloc_ptr_) & (align-1);   // 相當於對align取模
  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 { // AllocateFallback always returned aligned memory result = AllocateFallback(bytes); } assert((reinterpret_cast<uintptr_t>(result) & (align-1)) == 0); return result; }

總結:

leveldb實現的內存分配器還是很簡單的,有點簡陋的感覺。相對於leveldb只用一個vector維護,c++ stl所實現的默認的內存分配器就精細多了,它按8、16、32、...... 字節大小做了多級管理,當前級不能再使用的內存還可以供下一級使用,基本很少有內存浪費,不過也因此帶來了維護這個結構更高的復雜度,也需要額外保存更多的冗余信息。

leveldb 閱讀筆記(1) 內存分配