LevelDB 原始碼解析之 Arena

debugzhang發表於2021-03-29

GitHub: https://github.com/storagezhang

Emai: debugzhang@163.com

華為雲社群: https://bbs.huaweicloud.com/blogs/250328

記憶體池

記憶體池的存在主要就是減少呼叫 malloc 或者 new 的次數,減少記憶體分配所帶來的系統開銷,提升效能。

LevelDB 中的記憶體池是由類 Arena 實現的。Arena 先向系統申請一塊大的記憶體,當其他元件需要申請記憶體時,Arena 先將已有的記憶體塊分配給元件,如果不夠用則再申請一塊大的記憶體。當記憶體池物件析構時,分配的記憶體均被釋放,這保證了記憶體不會洩漏。

申請記憶體和分配記憶體的區別:

  • 申請記憶體:向作業系統申請一塊連續的記憶體空間。
  • 分配記憶體;將已經申請的記憶體分配給其他元件使用。

成員變數

// 指向當前記憶體塊未分配記憶體的起始地址的指標
char* alloc_ptr_;
// 記錄當前記憶體塊未分配記憶體的大小
size_t alloc_bytes_remaining_;

// 每個記憶體塊的地址都儲存在 vector 中
std::vector<char*> blocks_;

// 原子變數:記錄當前物件的記憶體總量
std::atomic<size_t> memory_usage_;

如圖所示,Arena 的成員變數 blocks_ 儲存若干個指標,每個指標指向一塊記憶體。alloc_ptr_ 指向當前記憶體塊未分配記憶體的起始地址,alloc_bytes_remaining_ 為當前記憶體塊未分配記憶體的大小。

static const int kBlockSize = 4096;

Arena 以記憶體塊為單位來管理記憶體,每個記憶體塊的大小 kBlockSize 為 4096 KB。

建構函式與解構函式

Arena::Arena()
    : alloc_ptr_(nullptr), alloc_bytes_remaining_(0), memory_usage_(0) {}

Arena::~Arena() {
  for (size_t i = 0; i < blocks_.size(); i++) {
    delete[] blocks_[i];
  }
}

建構函式初始化所有的成員變數,保證不會使用未初始化的變數。

解構函式釋放 blocks_ 中每個指標指向的記憶體塊。

記憶體分配介面

Arena 提供了 3 個 public 函式來簡化記憶體分配。

Arena 的記憶體分配策略有三種,當申請 bytes 大小的記憶體時:

  • 如果 bytes 小於等於當前記憶體塊剩餘記憶體,直接在當前記憶體塊上分配記憶體;
  • 如果 bytes 大於當前記憶體塊剩餘記憶體,呼叫 AllocateFallback 函式按照另外兩種分配策略分配記憶體。

Allocate

inline char* Arena::Allocate(size_t bytes) {
  // 不需要分配 0 位元組的記憶體
  assert(bytes > 0);

  // 申請的記憶體小於當前記憶體塊剩餘的記憶體,直接在當前記憶體塊上分配記憶體
  if (bytes <= alloc_bytes_remaining_) {
    char* result = alloc_ptr_;

    // 從當前記憶體塊中分配記憶體
    alloc_ptr_ += bytes;

    // 計算當前記憶體塊的剩餘記憶體大小
    alloc_bytes_remaining_ -= bytes;
    return result;
  }

  // 申請的記憶體大於當前記憶體塊剩餘的記憶體,使用 AllocateFallback 函式重新申請記憶體
  return AllocateFallback(bytes);
}

Allocate 函式分配 bytes 大小的記憶體空間,返回指向所分配記憶體的指標。

AllocateAligned

char* Arena::AllocateAligned(size_t bytes) {
  // 計算當前機器要對齊的位元組數,最多 8 位元組對齊,否則就按照當前機器的 void* 的大小來對齊
  const int align = (sizeof(void*) > 8) ? sizeof(void*) : 8;

  // 位元組對齊必須是 2 的次冪
  // x & (x - 1) = 0 表示 x 是 2 的次冪
  static_assert((align & (align - 1)) == 0,
                "Pointer size should be a power of 2");

  // A & (B - 1) = A % B
  // reinterpret_cast<uintptr_t> 型別對應機器指標大小
  size_t current_mod = reinterpret_cast<uintptr_t>(alloc_ptr_) & (align - 1);

  // 如果 current_mod = 0 表示 alloc_ptr_ 已經是位元組對齊的
  // 否則計算 align - current_mod,表示當前指標地址距離位元組對齊的偏差
  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 函式重新申請記憶體
    result = AllocateFallback(bytes);
  }

  // 保證分配的記憶體起始地址是位元組對齊的
  assert((reinterpret_cast<uintptr_t>(result) & (align - 1)) == 0);
  return result;
}

AllocateAligned 函式分配 bytes 大小的記憶體空間,且起始地址位元組對齊,返回指向所分配記憶體的指標。

MemoryUsage

size_t MemoryUsage() const {
  return memory_usage_.load(std::memory_order_relaxed);
}

MemoryUsage 函式返回當前分配給 Arena 物件的所有記憶體空間大小和所有指向記憶體塊的指標大小之和。

記憶體分配內部實現

接上節中的 Arena 的記憶體分配策略,當申請 bytes 大小的記憶體時:

  • 如果 bytes 小於等於當前記憶體塊剩餘記憶體,直接在當前記憶體塊上分配記憶體;
  • 如果 bytes 大於當前記憶體塊剩餘記憶體:
    • 如果 bytes 小於等於預設記憶體塊大小的四分之一,新申請一個記憶體塊,大小為預設記憶體塊大小,在該記憶體塊上分配記憶體;
    • 如果 bytes 大於預設記憶體塊大小的四分之一,新申請一個記憶體塊,大小為 bytes,分配記憶體。

AllocateFallback

char* Arena::AllocateFallback(size_t bytes) {
  // 呼叫 AllocateNewBlock 申請一塊大小為 bytes 的新記憶體塊
  if (bytes > kBlockSize / 4) {
    // 在新申請的記憶體塊中分配全部記憶體
    char* result = AllocateNewBlock(bytes);
    return result;
  }

  // 呼叫 AllocateNewBlock 申請一塊大小為 kBlockSize 的新記憶體塊
  alloc_ptr_ = AllocateNewBlock(kBlockSize);
  alloc_bytes_remaining_ = kBlockSize;

  // 在新申請的記憶體塊中分配 bytes 大小的記憶體
  char* result = alloc_ptr_;
  alloc_ptr_ += bytes;
  alloc_bytes_remaining_ -= bytes;
  return result;
}

當申請的記憶體大於當前記憶體塊剩餘記憶體時,AllocateFallback 函式會被呼叫,用來按照後兩種分配策略分配記憶體。

這兩種分配策略可以進一步減少記憶體分配的次數,但同時每塊最後 \(\frac{1}{4}\) 的空間有可能會被浪費。

AllocateNewBlock

char* Arena::AllocateNewBlock(size_t block_bytes) {
  // 申請一個大小為 block_bytes 的記憶體塊
  char* result = new char[block_bytes];

  // 將該記憶體塊的地址新增到 blocks 中
  blocks_.push_back(result);

  // 記錄當前物件記憶體分配總量
  memory_usage_.fetch_add(block_bytes + sizeof(char*),
                          std::memory_order_relaxed);
  return result;
}

AllocateNewBlock 函式申請一個大小為 block_bytes 的記憶體塊。

總結

當向 Arena 申請 bytes 大小的記憶體時:

  • 如果 bytes 小於等於當前記憶體塊剩餘記憶體,直接在當前記憶體塊上分配記憶體;
  • 如果 bytes 大於當前記憶體塊剩餘記憶體:
    • 如果 bytes 小於等於預設記憶體塊大小的四分之一,新申請一個記憶體塊,大小為預設記憶體塊大小,在該記憶體塊上分配記憶體;
    • 如果 bytes 大於預設記憶體塊大小的四分之一,新申請一個記憶體塊,大小為 bytes,分配記憶體。

相關文章