leveldb程式碼精讀 記憶體池Arena

liiinuuux發表於2015-11-12
程式碼檔案
util/arena.h
util/arena.cc

level db有個記憶體池不叫xxxpool,而叫Arena。
從作業系統申請記憶體的方式是直接new一個空的char陣列,得到一大塊連續記憶體作為資料塊,並維護資料塊內的空間使用情況。
當有app申請記憶體時,將陣列裡某個char的地址返回給app,讓它使用從這個char開始的一段連續記憶體。
當一個資料塊不能滿足app的連續記憶體需求時,就會建立新的char陣列。
每個char陣列,也就是資料塊的地址都記錄在Arena的一個vector裡。當Arena銷燬時,就根據vector裡記錄的資料塊地址逐個釋放空間。

成員變數

  1. std::vector<char*> blocks_; // 已分配的記憶體塊的地址佇列
  2. size_t blocks_memory_; // 已分配所有記憶體塊的總記憶體
  3. char* alloc_ptr_; // 當前記憶體塊下次分配的地址。比如已經分配了4個單位,alloc_ptr_就指向5
  4. size_t alloc_bytes_remaining_; // 當前塊內還剩多少記憶體
  5. static const int kBlockSize = 4096; // 預設的標準塊是4k,也支援按程式要求申請指定大小的塊



私有函式

  1. char* AllocateNewBlock(size_t block_bytes); //按照給定大小分配一個新資料塊,將新塊的地址推入blocks_
  2. char* AllocateFallback(size_t bytes); //當前塊的alloc_bytes_remaining_小於請求的記憶體大小時,用這個函式分配新記憶體



公共函式

  1. char* Allocate(size_t bytes); //從當前塊中分配記憶體,如果記憶體不夠就呼叫AllocateFallback
  2. char* AllocateAligned(size_t bytes); //和Allocate類似,但是這個會做記憶體對齊
  3. size_t MemoryUsage() const {... //返回當前已分配的記憶體塊。是估算值,以建立記憶體塊但是還沒分配給app使用的部分也算在內。



MemoryUsage函式的內容很簡單,就是返回blocks_memory_加上blocks_裡存放的指標大小之和

  1. size_t MemoryUsage() const {
  2.     return blocks_memory_ + blocks_.capacity() * sizeof(char*);
  3.   }


  

Arena最基本的操作是分配一個新記憶體塊。
只是申請一個塊新記憶體,並不更新alloc_ptr_和alloc_bytes_remaining_ 。
記憶體塊的形式就是簡單地以char陣列的形式佔用一片連續記憶體。

  1. char* Arena::AllocateNewBlock(size_t block_bytes) {
  2.   // 用空的char陣列佔用一片連續記憶體,將地址儲存到result裡,當做返回值。
  3.   char* result = new char[block_bytes];
  4.   // 整個Arena已分配的總記憶體需要加上新申請的塊的大小
  5.   blocks_memory_ += block_bytes;
  6.   // 將新塊的地址追加到blocks_佇列
  7.   blocks_.push_back(result);
  8.   返回新塊地址
  9.   return result;
  10. }



AllocateNewBlock只是最基本的分配新塊,沒辦法實用,比如給app分配記憶體時關鍵的alloc_ptr_ 和alloc_bytes_remaining_ 它不會維護。
Arena內部分配新塊的功能函式實際是AllocateFallback。透過AllocateFallback呼叫AllocateNewBlock

  1. char* Arena::AllocateFallback(size_t bytes) {
  2.   // 它會先看一下要分配多大記憶體。如果是大於標準塊的四分之一,就直接新建一個這麼大記憶體的非標準塊,給app專用了。
  3.   // 剩下的一起都有app自己解決,Arena保持當前塊的alloc_ptr_和alloc_bytes_remaining_,給以後分配小空間使用。
  4.   if (bytes > kBlockSize / 4) {
  5.     char* result = AllocateNewBlock(bytes);
  6.     return result;
  7.   }

  8.   // 如果申請的bytes小於標準塊的四分之一,並且剩餘空間alloc_bytes_remaining_還是不夠,就建立新的標準塊,將地址賦給alloc_ptr_。
  9.   // 新的標準塊有Arena維護,作為當前塊給app分配記憶體。alloc_bytes_remaining_自然就是標準塊大小。
  10.   alloc_ptr_ = AllocateNewBlock(kBlockSize);
  11.   alloc_bytes_remaining_ = kBlockSize;

  12.   char* result = alloc_ptr_; // 由於是新的標準塊,因此塊的起始地址就是給app分配記憶體的起始地址,作為返回值。
  13.   alloc_ptr_ += bytes; // alloc_ptr_後移bytes,作為以後分配記憶體的起始位置。
  14.   alloc_bytes_remaining_ -= bytes; // 塊內剩餘記憶體減少已分配給app的bytes
  15.   return result;
  16. }



外部app呼叫的是Allocate函式。
由於Arena記錄了當前可分配的起始地址alloc_ptr_,以及當前塊剩餘的記憶體alloc_bytes_remaining_,因此只需要操作這兩個值即可。
blocks_memory_在分配新塊的時候已經加過塊大小了,就不用每次分配小記憶體都去維護了。

  1. inline char* Arena::Allocate(size_t bytes) {
  2.   assert(bytes > 0);
  3.   if (bytes <= alloc_bytes_remaining_) {
  4.     char* result = alloc_ptr_; // alloc_ptr_是當前可分配記憶體的起始地址,肯定就是新分配記憶體的地址了。
  5.     alloc_ptr_ += bytes; // 分配後alloc_ptr_需要向後移bytes,執行下一次分配的起始地址。
  6.     alloc_bytes_remaining_ -= bytes; // 塊內剩餘記憶體減少了bytes
  7.     return result;
  8.   }
  9.   return AllocateFallback(bytes); // 如果app申請的記憶體大於塊內剩餘記憶體,就呼叫AllocateFallback來申請新塊。
  10. }



以上就是Arena自身建立資料塊,以及將資料塊內的空間分配給app使用的過程。
Arena裡還有一個函式,和Allocate一樣供外部app呼叫,區別是它會做記憶體對齊。
關於記憶體對齊,看下面一段程式。

  1. #include <stdio.h>
  2. #include <unistd.h>

  3. struct st {
  4.      char c;
  5.      int i;
  6. };

  7. int main()
  8. {
  9.      char c = 'A';
  10.      int i = 100;
  11.      struct st st1;
  12.      printf("size char : %d\n", sizeof(c));
  13.      printf("size int : %d\n", sizeof(i));
  14.      printf("size st : %d\n", sizeof(st1));
  15.      printf("addr st1.c : %x\n", &(st1.c));
  16.      printf("addr st1.i : %x\n", &(st1.i));
  17.      return 0;
  18. }
輸出結果
[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函式

  1. char* Arena::AllocateAligned(size_t bytes) {
  2.   const int align = (sizeof(void*) > 8) ? sizeof(void*) : 8;
  3.   assert((align & (align-1)) == 0); // Pointer size should be a power of 2
  4.   size_t current_mod = reinterpret_cast<uintptr_t>(alloc_ptr_) & (align-1);
  5.   size_t slop = (current_mod == 0 ? 0 : align - current_mod);
  6.   size_t needed = bytes + slop;
  7.   char* result;
  8.   if (needed <= alloc_bytes_remaining_) {
  9.     result = alloc_ptr_ + slop;
  10.     alloc_ptr_ += needed;
  11.     alloc_bytes_remaining_ -= needed;
  12.   } else {
  13.     // AllocateFallback always returned aligned memory
  14.     result = AllocateFallback(bytes);
  15.   }
  16.   assert((reinterpret_cast<uintptr_t>(result) & (align-1)) == 0);
  17.   return result;
  18. }



Allocate做判斷時直接拿app申請的記憶體大小bytes與alloc_bytes_remaining_比較。
而AllocateAligned是先把bytes對齊,然後再和alloc_bytes_remaining_比較。
下面逐行分析記憶體對齊的過程

  1. const int align = (sizeof(void*) > 8) ? sizeof(void*) : 8;
首先需要確認按多少對齊。必須保證對齊單位必須能放下一個完整的指標(void* ),如果指標的體積小於8,就按8位元組對齊。
在我的環境裡sizeof(void*)就是8位元組,下面都已8位元組為例。


  1. assert((align & (align-1)) == 0);
這句是要求這個對齊單位必須是2的n次方。也即是說它的2進位制必須是一個1後面跟的全是0。
比如從8開始
8: 1000
16:10000
...
...


  1. 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。


  1. size_t needed = bytes + slop;
app請求的記憶體bytes加上需要填補的slop,就得到了實際需要分配的記憶體量。


  1. char* result;
  2.   if (needed <= alloc_bytes_remaining_) {
  3.      result = alloc_ptr_ + slop;
  4. ...
下面就和Allocate函式差不多了,只是返回給app的地址result不是alloc_ptr_ ,而是alloc_ptr_ 加上補充的空位元組後的。





來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/26239116/viewspace-1832774/,如需轉載,請註明出處,否則將追究法律責任。

相關文章