LevelDB 源碼解析之 Arena

GitHub: https://github.com/storagezhangc++

Emai: debugzhang@163.comgit

華爲雲社區: https://bbs.huaweicloud.com/blogs/250328github

LevelDB: https://github.com/google/leveldb函數

內存池

內存池的存在主要就是減小調用 malloc 或者 new 的次數,減小內存分配所帶來的系統開銷,提高性能。性能

LevelDB 中的內存池是由類 Arena 實現的。Arena 先向系統申請一塊大的內存,當其餘組件須要申請內存時,Arena 先將已有的內存塊分配給組件,若是不夠用則再申請一塊大的內存。當內存池對象析構時,分配的內存均被釋放,這保證了內存不會泄漏。fetch

申請內存和分配內存的區別:ui

  • 申請內存:向操做系統申請一塊連續的內存空間。
  • 分配內存;將已經申請的內存分配給其餘組件使用。

成員變量

// 指向當前內存塊未分配內存的起始地址的指針
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_ 爲當前內存塊未分配內存的大小。google

static const int kBlockSize = 4096;

Arena 之內存塊爲單位來管理內存,每一個內存塊的大小 kBlockSize 爲 4096 KB。atom

構造函數與析構函數

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];
  }
}

構造函數初始化全部的成員變量,保證不會使用未初始化的變量。spa

析構函數釋放 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,分配內存。
相關文章
相關標籤/搜索