leveldb程式碼精讀 記憶體池Arena
程式碼檔案
util/arena.h
util/arena.cc
level db有個記憶體池不叫xxxpool,而叫Arena。
從作業系統申請記憶體的方式是直接new一個空的char陣列,得到一大塊連續記憶體作為資料塊,並維護資料塊內的空間使用情況。
當有app申請記憶體時,將陣列裡某個char的地址返回給app,讓它使用從這個char開始的一段連續記憶體。
當一個資料塊不能滿足app的連續記憶體需求時,就會建立新的char陣列。
每個char陣列,也就是資料塊的地址都記錄在Arena的一個vector裡。當Arena銷燬時,就根據vector裡記錄的資料塊地址逐個釋放空間。
成員變數
-
std::vector<char*> blocks_; // 已分配的記憶體塊的地址佇列
-
size_t blocks_memory_; // 已分配所有記憶體塊的總記憶體
-
char* alloc_ptr_; // 當前記憶體塊下次分配的地址。比如已經分配了4個單位,alloc_ptr_就指向5
-
size_t alloc_bytes_remaining_; // 當前塊內還剩多少記憶體
- static const int kBlockSize = 4096; // 預設的標準塊是4k,也支援按程式要求申請指定大小的塊
私有函式
-
char* AllocateNewBlock(size_t block_bytes); //按照給定大小分配一個新資料塊,將新塊的地址推入blocks_
- char* AllocateFallback(size_t bytes); //當前塊的alloc_bytes_remaining_小於請求的記憶體大小時,用這個函式分配新記憶體
公共函式
-
char* Allocate(size_t bytes); //從當前塊中分配記憶體,如果記憶體不夠就呼叫AllocateFallback
-
char* AllocateAligned(size_t bytes); //和Allocate類似,但是這個會做記憶體對齊
- size_t MemoryUsage() const {... //返回當前已分配的記憶體塊。是估算值,以建立記憶體塊但是還沒分配給app使用的部分也算在內。
MemoryUsage函式的內容很簡單,就是返回blocks_memory_加上blocks_裡存放的指標大小之和
-
size_t MemoryUsage() const {
-
return blocks_memory_ + blocks_.capacity() * sizeof(char*);
- }
Arena最基本的操作是分配一個新記憶體塊。
只是申請一個塊新記憶體,並不更新alloc_ptr_和alloc_bytes_remaining_ 。
記憶體塊的形式就是簡單地以char陣列的形式佔用一片連續記憶體。
-
char* Arena::AllocateNewBlock(size_t block_bytes) {
-
// 用空的char陣列佔用一片連續記憶體,將地址儲存到result裡,當做返回值。
-
char* result = new char[block_bytes];
-
// 整個Arena已分配的總記憶體需要加上新申請的塊的大小
-
blocks_memory_ += block_bytes;
-
// 將新塊的地址追加到blocks_佇列
-
blocks_.push_back(result);
-
返回新塊地址
-
return result;
- }
AllocateNewBlock只是最基本的分配新塊,沒辦法實用,比如給app分配記憶體時關鍵的alloc_ptr_ 和alloc_bytes_remaining_ 它不會維護。
Arena內部分配新塊的功能函式實際是AllocateFallback。透過AllocateFallback呼叫AllocateNewBlock
-
char* Arena::AllocateFallback(size_t bytes) {
-
// 它會先看一下要分配多大記憶體。如果是大於標準塊的四分之一,就直接新建一個這麼大記憶體的非標準塊,給app專用了。
-
// 剩下的一起都有app自己解決,Arena保持當前塊的alloc_ptr_和alloc_bytes_remaining_,給以後分配小空間使用。
-
if (bytes > kBlockSize / 4) {
-
char* result = AllocateNewBlock(bytes);
-
return result;
-
}
-
-
// 如果申請的bytes小於標準塊的四分之一,並且剩餘空間alloc_bytes_remaining_還是不夠,就建立新的標準塊,將地址賦給alloc_ptr_。
-
// 新的標準塊有Arena維護,作為當前塊給app分配記憶體。alloc_bytes_remaining_自然就是標準塊大小。
-
alloc_ptr_ = AllocateNewBlock(kBlockSize);
-
alloc_bytes_remaining_ = kBlockSize;
-
-
char* result = alloc_ptr_; // 由於是新的標準塊,因此塊的起始地址就是給app分配記憶體的起始地址,作為返回值。
-
alloc_ptr_ += bytes; // alloc_ptr_後移bytes,作為以後分配記憶體的起始位置。
-
alloc_bytes_remaining_ -= bytes; // 塊內剩餘記憶體減少已分配給app的bytes
-
return result;
- }
外部app呼叫的是Allocate函式。
由於Arena記錄了當前可分配的起始地址alloc_ptr_,以及當前塊剩餘的記憶體alloc_bytes_remaining_,因此只需要操作這兩個值即可。
blocks_memory_在分配新塊的時候已經加過塊大小了,就不用每次分配小記憶體都去維護了。
-
inline char* Arena::Allocate(size_t bytes) {
-
assert(bytes > 0);
-
if (bytes <= alloc_bytes_remaining_) {
-
char* result = alloc_ptr_; // alloc_ptr_是當前可分配記憶體的起始地址,肯定就是新分配記憶體的地址了。
-
alloc_ptr_ += bytes; // 分配後alloc_ptr_需要向後移bytes,執行下一次分配的起始地址。
-
alloc_bytes_remaining_ -= bytes; // 塊內剩餘記憶體減少了bytes
-
return result;
-
}
-
return AllocateFallback(bytes); // 如果app申請的記憶體大於塊內剩餘記憶體,就呼叫AllocateFallback來申請新塊。
- }
以上就是Arena自身建立資料塊,以及將資料塊內的空間分配給app使用的過程。
Arena裡還有一個函式,和Allocate一樣供外部app呼叫,區別是它會做記憶體對齊。
關於記憶體對齊,看下面一段程式。
-
#include <stdio.h>
-
#include <unistd.h>
-
-
struct st {
-
char c;
-
int i;
-
};
-
-
int main()
-
{
-
char c = 'A';
-
int i = 100;
-
struct st st1;
-
printf("size char : %d\n", sizeof(c));
-
printf("size int : %d\n", sizeof(i));
-
printf("size st : %d\n", sizeof(st1));
-
printf("addr st1.c : %x\n", &(st1.c));
-
printf("addr st1.i : %x\n", &(st1.i));
-
return 0;
- }
[root@mysql1 c]# ./a
size char : 1
size int : 4
size st : 8
addr st1.c : a5334f40
addr st1.i : a5334f44
從結果看,char佔1個位元組,int佔四個。但是結構體的體積確實8。
列印結構體內裡char c和int i的地址,會發現char c實際佔用了4位元組。
上面就是系統自動做了記憶體對齊,在char c後面填了3位元組。
Arena在建立new char[block_bytes]這麼大的塊後,需要在裡面為app分配記憶體。
由於塊內的空間是Arena自己維護的,系統不會做記憶體對齊,就需要Arena自己對齊。
下面是Arena記憶體對齊版的Allocate函式
-
char* Arena::AllocateAligned(size_t bytes) {
-
const int align = (sizeof(void*) > 8) ? sizeof(void*) : 8;
-
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;
- }
Allocate做判斷時直接拿app申請的記憶體大小bytes與alloc_bytes_remaining_比較。
而AllocateAligned是先把bytes對齊,然後再和alloc_bytes_remaining_比較。
下面逐行分析記憶體對齊的過程
- const int align = (sizeof(void*) > 8) ? sizeof(void*) : 8;
首先需要確認按多少對齊。必須保證對齊單位必須能放下一個完整的指標(void* ),如果指標的體積小於8,就按8位元組對齊。
在我的環境裡sizeof(void*)就是8位元組,下面都已8位元組為例。
- assert((align & (align-1)) == 0);
這句是要求這個對齊單位必須是2的n次方。也即是說它的2進位制必須是一個1後面跟的全是0。
比如從8開始
8: 1000
16:10000
...
...
- size_t current_mod = reinterpret_cast<uintptr_t>(alloc_ptr_) & (align-1);
這句的意思是看看alloc_ptr_的“零頭”是多少
以10進製為例
如果以10為標準(align = 10),大於等於10的部分都不用看,只看小於10(也就是align-1)的部分有沒有值。因此108的零頭是8。
換到2進位制
1 當前分配的起始地址是alloc_ptr_,把alloc_ptr_強行轉換成長整形。
2 對齊單位以8為例,align-1就是7,也就是111
3 reinterpret_cast<uintptr_t>(alloc_ptr_) & (align-1)
由於align-1 只有低三位是1,其它全是0。和它按位與過之後,大於align-1的部分就全沒了,只保留“零頭”
alloc_ptr_作為地址,它的二進位制可能是 ...101111010這種形式
計算後就得到了“零頭”
...10111010
& ...00000111
--------------
...00000010
4 計算之後,發現上次分配後,多出的“零頭” current_mod = 2
size_t slop = (current_mod == 0 ? 0 : align - current_mod);
用對其單位align減去零頭,就得到了需要手工填補的位元組數。
上面例子裡的結構體,char c是1位元組,對齊單位是4,就填補了4-1=3位元組。
我們的對齊單位是align,因此需要填align - "零頭"current_mod。
- size_t needed = bytes + slop;
-
char* result;
-
if (needed <= alloc_bytes_remaining_) {
-
result = alloc_ptr_ + slop;
- ...
下面就和Allocate函式差不多了,只是返回給app的地址result不是alloc_ptr_ ,而是alloc_ptr_ 加上補充的空位元組後的。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/26239116/viewspace-1832774/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- leveldb原始碼分析(1)--arena記憶體池的實現原始碼記憶體
- LevelDB學習筆記 (3): 長文解析memtable、跳錶和記憶體池Arena筆記記憶體
- leveldb程式碼精讀 插入操作
- leveldb程式碼精讀 lru cache
- LevelDB 原始碼解析之 Arena原始碼
- 記憶體池、程式池、執行緒池記憶體執行緒
- leveldb 程式碼閱讀三
- leveldb程式碼精讀 資料庫啟動和初始化資料庫
- Swoole 原始碼分析——記憶體模組之記憶體池原始碼記憶體
- 【恩墨學院】當Java虛擬機器遇上Linux Arena記憶體池Java虛擬機Linux記憶體
- [筆記] 解碼Nginx:記憶體池(Memory Pool)筆記Nginx記憶體
- Linux 記憶體池原始碼淺析Linux記憶體原始碼
- 記憶體池設計記憶體
- Netty原始碼解析 -- 記憶體池與PoolArenaNetty原始碼記憶體
- Go 語言社群新提案 arena,可優化記憶體分配Go優化記憶體
- 精講Redis記憶體模型Redis記憶體模型
- 7.7 實現程式記憶體讀寫記憶體
- C++記憶體管理:簡易記憶體池的實現C++記憶體
- 記憶體池原理大揭祕記憶體
- C++手寫記憶體池C++記憶體
- Go1.20 arena 能手動管理記憶體了,怎麼用?Go記憶體
- mimalloc記憶體分配程式碼分析記憶體
- ResNet程式碼精讀
- Flutter引擎原始碼解讀-記憶體管理篇Flutter原始碼記憶體
- iOS記憶體管理和malloc原始碼解讀iOS記憶體原始碼
- LevelDB 程式碼擼起來!
- GC最佳化:棧記憶體、span、NativeMemory、指標、池化記憶體 筆記GC記憶體指標筆記
- 分析高效記憶體池的實現方式記憶體
- Ogre記憶體池的使用和說明記憶體
- [轉帖]深入JVM - Code Cache記憶體池JVM記憶體
- PostgreSQL 原始碼解讀(229)- Linux Kernel(程式虛擬記憶體#3)SQL原始碼Linux記憶體
- PostgreSQL 原始碼解讀(227)- Linux Kernel(程式虛擬記憶體#2)SQL原始碼Linux記憶體
- Linux讀寫實體記憶體Linux記憶體
- 【精選】Mac 手動記憶體清理教程Mac記憶體
- 物理讀和記憶體讀較高SQL記憶體SQL
- JVM原始碼分析之堆外記憶體完全解讀JVM原始碼記憶體
- ThreadLocal原始碼解讀和記憶體洩露分析thread原始碼記憶體洩露
- Python原始碼閱讀-記憶體管理機制(一)Python原始碼記憶體