LevelDB 源码分析【1】——内存管理 Arena

看了网上的资料,突然发现一个很好用的工具 gperftools,它实现了一套高性能的 malloc,除此之外还提供了一些性能分析工具,能够对 heap,cpu 进行分析;

如果是这样那还不能让我狂喜;真正的价值在于,它既然能对 heap 进行分析,就能图形化地显示出整个程序执行进程,这对于阅读大型源代码来说无疑是有力的助手啊!!!

安装 gperftools

体系结构:x86

环境:任意 Linux 系统

直接去 github 上查看安装流程 https://github.com/gperftools/gperftools

由于官方文档中提到,可能系统自带的 libunwind 会引发一些 bug,因此直接去 github 安装最新的 libunwind https://github.com/libunwind/libunwind

1
2
3
4
5
6
$ git clone https://github.com/libunwind/libunwind
$ cd libunwind/
$ autoreconf -i
$ ./configure
$ make
$ make install prefix=/usr/local

接下来就可以安装 gperftools

1
2
3
4
5
6
$ git clone https://github.com/gperftools/gperftools
$ cd gperftools/
$ ./autogen.sh
$ ./configure
$ make
$ make install prefix=/usr/local

ubuntu 安装图形化工具

1
$ sudo apt install graphviz ghostscript

测试 leveldb 的 heapprofile

开启一个 leveldb 数据库,并向里面大量写数据;这样就可以看到哪个函数去分配了大量内存,以及分配内存的整个函数调用过程;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// test_leveldb.cpp
#include <leveldb/db.h>
#include <iostream>
using namespace std;

int main() {
leveldb::DB *db;
leveldb::Options options;

options.create_if_missing = true;

leveldb::DB::Open(options, "/tmp/testdb", &db);

string key = "MyKey29", value = "Hello World!", result;

string prefix(100, 'c');

for (int i = 0;i < 12000000; ++i) {
key = prefix + to_string(i);
db->Put(leveldb::WriteOptions(), key, value + to_string(i));
}

delete db;
return 0;
}

官方文档给出了几种使用 gperftools 的方式,见 https://gperftools.github.io/gperftools/cpuprofile.html

我是直接使用动态链接的方式,把 libperftools.so 链接到自己的可执行文件上

1
$ g++ -g test_leveldb.cpp -o test_leveldb -lleveldb -lpthread -ltcmalloc -lprofiler -Wl,-rpath=/usr/local/lib

然后通过环境变量来开启 heap 分析

1
2
$ mkdir -p /tmp/code/leveldb
$ HEAPPROFILE="/tmp/code/leveldb/run" ./test_leveldb

最后就可以使用 pprof 工具对输出的结果进行分析了

1
$ pprof --pdf run run.0001.heap >run.pdf

最终结果如图所示:

0.PNG

图太大,就截了重要的部分,可以看到最终内存的分配都是 AllocateNewBlock 去做的,并且有两条路能够到到这个函数

  • Allocate
  • AllocateAligned

接下来就根据图示流程来分析下具体的函数

源码分析

Arena::Allocate

1
2
3
4
5
6
7
8
9
10
11
12
13
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);
}

Arena::AllocateAligned

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
char* Arena::AllocateAligned(size_t bytes) {
const int align = (sizeof(void*) > 8) ? sizeof(void*) : 8;
static_assert((align & (align - 1)) == 0,
"Pointer size should be a power of 2");
size_t current_mod = reinterpret_cast<uintptr_t>(alloc_ptr_) & (align - 1);
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;
}

Arena::AllocateFallback

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;
}