GitHub: https://github.com/storagezhang
Emai: debugzhang@163.com
華為雲社區: https://bbs.huaweicloud.com/blogs/250328
LevelDB: https://github.com/google/leveldb
內存池
內存池的存在主要就是減少調用 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
,分配內存。
- 如果