内存管理工具Arena
在Rocksdb中提供了两大类内存管理工具,分别是Allocator和MemoryAllocator,在这里不详细分析MemAllocator的实现,因为它仅仅是jemalloc 的wrapper。
Allocator首先是一个虚基类,定义了三类接口
// 这里支持对齐和不对齐两种方式
// 因为我们申请了一个又一个的block然后自己管理内存
// 因此必须自己处理内存对齐的问题
// 分配bytes大小的字节
virtual char* Allocate(size_t bytes) = 0;
// 按照bytes大小对齐过后的字节
virtual char* AllocateAligned(size_t bytes, size_t huge_page_size = 0,
Logger* logger = nullptr) = 0;
// 返回当前一个块的大小
virtual size_t BlockSize() const = 0;
Allocator下有两个实现,一个是Arena另一个是Concurrent_Arena,在这里先讨论非并发版本,因为并发版本是Arena的wrapper
首先我们需要一定的预备知识:hugePage
HugePage 巨页技术
众所周知x86架构下我们默认使用4KB的内存页,通常内存页过大会直接导致对内存利用率的下降,因为我们每次分配内存的时候至少分配一个内存页。而内存页过小会导致页表膨胀,并且TLB的查找速度下降。4KB是一个有历史原因的合理数字,使用4KB未见得就比8KB或者16KB更好。
它的优势:
- 对于存储同样entries的TLB,如果使用了巨页那就意味着更低的TLB miss。
- 对于更大的内存页,它可能会减少页表的级别,那就意味着更少的访存次数。
- 减少页表的内存开销。
它的缺点:
- 内存操作基本都要求页为单位,例如加载一个动态链接库,可能浪费大量的巨页空间。
- OS运行一段时间之后可能不太能分配出大的内存页,需要像DMA一样在启动的时候就预留(这里说的是内核中高端内存的ZONE_DMA16)
- 动态hugePage换出会更慢
// Arena的成员变量
class Arena : public Allocator {
public:
static const size_t kInlineSize = 2048;
static const size_t kMinBlockSize;
static const size_t kMaxBlockSize;
private:
// 这里使用了__attribute__机制,是GNUC下独有的
// 使用它可以在后面的括号里设置函数、变量、类的属性
// 这里设置了inline_block_[kInlineSize]的对齐
// 通过max_align_t获取当前平台下与至少任何一种标量类型都一样大的对齐
// 通过alignof获得之后__aligned__设置
// 这里使用了栈
char inline_block_[kInlineSize] __attribute__((__aligned__(alignof(max_align_t))));
// 每次分配的block的字节数量
const size_t kBlockSize;
// 使用new[] 分配的block集合
// 每一个char*代表一个block
using Blocks = std::vector<char*>;
Blocks blocks_;
// 内部结构体,用来记录mmap分配的情况
// 这里的mmap只使用MAP_PAGETLB来分配大页内存
struct MmapInfo {
void* addr_;
size_t length_;
MmapInfo(void* addr, size_t length) : addr_(addr), length_(length) {}
};
// 使用mmap分配的block集合
std::vector<MmapInfo> huge_blocks_;
// 记录不规则的块数量,当有一次较大分配的时候,就要分配irregular_block
size_t irregular_block_num = 0;
// 这里有两个指针分别指向一个block的首(低)地址和尾(高)地址
// 每当分配的时候aligned_alloc_ptr指向的首地址将会分配需要对齐的内存
// unaligned_alloc_ptr将会分配不需要对齐的内存
// 如果将不对齐的内存放在一端分配显然会节省空间,而如果和对齐的放在一起分配
// 那相当于不对齐的内存也被迫对齐了,浪费了空间
char* unaligned_alloc_ptr_ = nullptr;
char* aligned_alloc_ptr_ = nullptr;
// 当前block当中还剩下多少的bytes
size_t alloc_bytes_remaining_ = 0;
#ifdef MAP_HUGETLB
// 使用mmap给block分配的内存的大小
size_t hugetlb_size_ = 0;
#endif // MAP_HUGETLB
// 目前为止在这一block分配了多少bytes
size_t blocks_memory_ = 0;
// AllocTracker跟踪内存分配情况
AllocTracker* tracker_;
};
通过上述不难发现的是:我们有三种方式来分配一个block的内存。
- 通过预设的kInlineSize来分配,我必定分配了2048 Bytes并且保证它们的对齐。
- 通过new[]来分配
- 通过mmap来分配
// 然后我们看看Arena都提供了什么方法
class Arena : public Allocator {
public:
// 显然不允许有拷贝操作
Arena(const Arena&) = delete;
void operator=(const Arena&) = delete;
// 这里涉及了huge_page,我们会先从huge_page中尝试分配,如果分配失败再普通分配
// 使用huge_page肯定要确保在支持huge_page的OS中
explicit Arena(size_t block_size = kMinBlockSize,
AllocTracker* tracker = nullptr, size_t huge_page_size = 0);
~Arena();
char* Allocate(size_t bytes) override;
// 使用巨页技术的、对齐的内存分配方法
char* AllocateAligned(size_t bytes, size_t huge_page_size = 0,
Logger* logger = nullptr) override;
// 返回估计的总的内存使用,不包含那些已经分配但是没有使用的,为了未来分配的内存
size_t ApproximateMemoryUsage() const {
return blocks_memory_ + blocks_.capacity() * sizeof(char*) -
alloc_bytes_remaining_;
}
// 总共分配了多少字节
size_t MemoryAllocatedBytes() const { return blocks_memory_; }
// 分配但是还没有使用的字节数量
size_t AllocatedAndUnused() const { return alloc_bytes_remaining_; }
// 如果一次分配太大了,那会分配一个大小相同的不对齐的块(还是不规则的块)
// 返回这样的块的数量
size_t IrregularBlockNum() const { return irregular_block_num; }
// 返回一个block的大小
size_t BlockSize() const override { return kBlockSize; }
// 是否使用了栈上的内存
bool IsInInlineBlock() const {
return blocks_.empty();
}
private:
char* AllocateFromHugePage(size_t bytes);
char* AllocateFallback(size_t bytes, bool aligned);
char* AllocateNewBlock(size_t block_bytes);
};
// 这里有个extern的函数
// 这个函数用来规范kBlockSize,让它处在kMinBlockSize与kMaxBlockSize之间
// 并且保证block_size是align的整数倍
extern size_t OptimizeBlockSize(size_t block_size);
这里主要提供的方法有:
- 构造函数
Arena::Arena(size_t block_size, AllocTracker* tracker, size_t huge_page_size)
: kBlockSize(OptimizeBlockSize(block_size)), tracker_(tracker) {
// 这里的assert实际上已经通过OptimizeBlockSize函数保证了kBlockSize的正确性了
// 多此一举,降低效率
assert(kBlockSize >= kMinBlockSize && kBlockSize <= kMaxBlockSize &&
kBlockSize % kAlignUnit == 0);
TEST_SYNC_POINT_CALLBACK("Arena::Arena:0", const_cast<size_t*>(&kBlockSize));
// 不难发现,此时基本都使用栈上的内存
alloc_bytes_remaining_ = sizeof(inline_block_);
blocks_memory_ += alloc_bytes_remaining_;
aligned_alloc_ptr_ = inline_block_;
unaligned_alloc_ptr_ = inline_block_ + alloc_bytes_remaining_;
#ifdef MAP_HUGETLB
hugetlb_size_ = huge_page_size;
// hugetlb_size向上取整数
if (hugetlb_size_ && kBlockSize > hugetlb_size_) {
hugetlb_size_ = ((kBlockSize - 1U) / hugetlb_size_ + 1U) * hugetlb_size_;
}
#else
(void)huge_page_size;
#endif
if (tracker_ != nullptr) {
tracker_->Allocate(kInlineSize);
}
}
- AllocateNewBlock(size_t block_bytes)
// 这里直接使用new分配内存,而没有分配align整数倍大小的内存
char* Arena::AllocateNewBlock(size_t block_bytes) {
// 这一句是一个小trick,通过emplace_back保留一个指针的空间
// 因为这个blocks_是一个指向block指针的集合
// emplace_back一个指针来为后面new得到的指针保留空间
// 这里先emplace_back而后new是因为如果先new而new崩了,那么不需要处理
// 可是如果new成功了而emplace_back崩了,那就要处理new之后的那段内存
// 否则就会造成内存泄露,也就是说如果先new然而emplace_back那就要在emplace_back
// 加上try_catch,而这种写法就不需要!
// 这里不使用reserve而是使用emplace_back是希望vector能自己管理capacity
// 不要人为插手,因为每次都reserve一下会导致重复的分配内存
blocks_.emplace_back(nullptr);
char* block = new char[block_bytes];
size_t allocated_size;
#ifdef ROCKSDB_MALLOC_USABLE_SIZE
allocated_size = malloc_usable_size(block);
#ifndef NDEBUG
// It's hard to predict what malloc_usable_size() returns.
// A callback can allow users to change the costed size.
std::pair<size_t*, size_t*> pair(&allocated_size, &block_bytes);
TEST_SYNC_POINT_CALLBACK("Arena::AllocateNewBlock:0", &pair);
#endif // NDEBUG
#else
allocated_size = block_bytes;
#endif // ROCKSDB_MALLOC_USABLE_SIZE
blocks_memory_ += allocated_size;
if (tracker_ != nullptr) {
tracker_->Allocate(allocated_size);
}
blocks_.back() = block;
return block;
}
- AllocateFromHugePage(size_t bytes)
char* Arena::AllocateFromHugePage(size_t bytes) {
#ifdef MAP_HUGETLB
if (hugetlb_size_ == 0) {
return nullptr;
}
// 这里的trick和上面的哪个是一样的
huge_blocks_.emplace_back(nullptr /* addr */, 0 /* length */);
void* addr = mmap(nullptr, bytes, (PROT_READ | PROT_WRITE),
(MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB), -1, 0);
if (addr == MAP_FAILED) {
return nullptr;
}
huge_blocks_.back() = MmapInfo(addr, bytes);
blocks_memory_ += bytes;
if (tracker_ != nullptr) {
tracker_->Allocate(bytes);
}
return reinterpret_cast<char*>(addr);
#else
(void)bytes;
return nullptr;
#endif
}
- AllocateFallback(size_t bytes, bool aligned)
// 上述两个函数往往被这个函数调用,它们被用来分配内存
// 这个函数是用来当前block内存不足的时候分配一个新的block
// 同时它接收bytes用来分配block的时候就把造成block不足的对象也给他分配了
char* Arena::AllocateFallback(size_t bytes, bool aligned) {
if (bytes > kBlockSize / 4) {
++irregular_block_num;
// 如果一个对象的大小已经超过了四分之一block
// 那么将它分开存放,以免浪费过多的block的内存
// 因为实际上上一个block还有很多内存,我们不应该抛弃它
// 实际上通过new给他一个新的内存空间
return AllocateNewBlock(bytes);
}
size_t size = 0;
char* block_head = nullptr;
#ifdef MAP_HUGETLB
if (hugetlb_size_) {
size = hugetlb_size_;
block_head = AllocateFromHugePage(size);
}
#endif
// 如果没使用hugetlb那就使用new分配
if (!block_head) {
size = kBlockSize;
block_head = AllocateNewBlock(size);
}
// 把造成block不足的对象也给他分配了
alloc_bytes_remaining_ = size - bytes;
// 这里修改aligned_alloc_ptr和unaligned_alloc_ptr代表开始使用这一个block
if (aligned) {
aligned_alloc_ptr_ = block_head + bytes;
unaligned_alloc_ptr_ = block_head + size;
return block_head;
} else {
aligned_alloc_ptr_ = block_head;
unaligned_alloc_ptr_ = block_head + size - bytes;
return unaligned_alloc_ptr_;
}
}
- AllocateAligned(size_t bytes, size_t huge_page_size,Logger* logger)
char* Arena::AllocateAligned(size_t bytes, size_t huge_page_size,
Logger* logger) {
// 位运算我总是搞不好,看看人家的操作
// 这里保证AlignUnit是2的k次幂
assert((kAlignUnit & (kAlignUnit - 1)) == 0);
#ifdef MAP_HUGETLB
// 这里的huge_page_size是自定义的内存对齐值
// 如果是0就表示使用默认对齐值
if (huge_page_size > 0 && bytes > 0) {
// Allocate from a huge page TLB table.
assert(logger != nullptr); // logger need to be passed in.
// 将需要的上调到huge_page_size对齐值的整数倍
size_t reserved_size =
((bytes - 1U) / huge_page_size + 1U) * huge_page_size;
assert(reserved_size >= bytes);
char* addr = AllocateFromHugePage(reserved_size);
if (addr == nullptr) {
ROCKS_LOG_WARN(logger,
"AllocateAligned fail to allocate huge TLB pages: %s",
errnoStr(errno).c_str());
// fail back to malloc
} else {
return addr;
}
}
#else
(void)huge_page_size;
(void)logger;
#endif
// 先计算对齐获得needed
size_t current_mod =
reinterpret_cast<uintptr_t>(aligned_alloc_ptr_) & (kAlignUnit - 1);
size_t slop = (current_mod == 0 ? 0 : kAlignUnit - current_mod);
size_t needed = bytes + slop;
char* result;
// 如果所需的没超过alloc_bytes_remaining那就分配
if (needed <= alloc_bytes_remaining_) {
result = aligned_alloc_ptr_ + slop;
aligned_alloc_ptr_ += needed;
alloc_bytes_remaining_ -= needed;
} else {
// 超过了就使用AllocateFallback分配一个block
result = AllocateFallback(bytes, true /* aligned */);
}
assert((reinterpret_cast<uintptr_t>(result) & (kAlignUnit - 1)) == 0);
return result;
}
到此为止可以说一下allocateAligned的分配逻辑了:
- 通过Arena的构造函数它会有一个初始的block在栈上,然后通过allocateAligned分配的时候
- 检查一下是否使用了hugePageTLB,如果使用了一定通过mmap来分配对象
- 如果没有使用就先分配栈上空间,当栈上面空间不够的时候进入AllocateFallback
- 然后看是不是这个对象太大了,如果是通过new分配一个irregularBlock
- 如果不是这个时候又检查是否开启了hugePageTLB,如果开了就通过mmap分配对象(这里又检查了一遍是为了下面实现allocate方便)
- 然后通过new分配一个新的Block并且将那个对象分配,转而使用新的block
- Allocate(size_t bytes)
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).
// 不允许0字节分配
// 不难看出就是先使用栈上的
// 上面不够了进入Fallback
assert(bytes > 0);
if (bytes <= alloc_bytes_remaining_) {
unaligned_alloc_ptr_ -= bytes;
alloc_bytes_remaining_ -= bytes;
return unaligned_alloc_ptr_;
}
return AllocateFallback(bytes, false /* unaligned */);
}
两头处理对齐与非对齐,防止内存泄露的小trick,确保是2的k次幂的方法