Inside of Jemalloc

来源:http://www.cnblogs.com/vector03/archive/2016/02/05/5182730.html
-Advertisement-
Play Games

Jemalloc最初是Jason Evans為FreeBSD開發的新一代記憶體分配器, 用來替代原來的 phkmalloc, 最早投入使用是在2005年. 到目前為止, 除了原版Je, 還有很多變種 被用在各種項目里. Google在android5.0里將bionic中的預設分配器從Dl替換為Je,...


INSIDE OF JEMALLOC
The Algorithm and Implementation of Jemalloc

author: vector03
mail:   [email protected]


--[ Table of contents

1 - 簡介
 2 - Basic structures
   2.1 - Overview
   2.2 - Arena (arena_t)
     2.2.1 - CPU Cache-Line
     2.2.2 - Arena原理
     2.2.3 - choose_arena
     2.2.4 - Arena結構
   2.3 - Chunk (arena_chunk_t)
     2.3.1 - overview
     2.3.2 - chunk結構
     2.3.3 - chunk map (arena_chunk_map_t)
   2.4 - Run (arena_run_t)
     2.4.1 - run結構
     2.4.2 - size class
     2.4.3 - size2bin/bin2size
   2.5 - Bins (arena_bin_t)
   2.6 - Thread caches (tcache_t)
   2.7 - Extent Node (extent_node_t)
   2.8 - Base
 3 - Allocation
   3.1 - Overview
   3.2 - Initialize
   3.3 - small allocation (Arena)
     3.3.1 - arena_run_reg_alloc
     3.3.2 - arena_bin_malloc_hard
     3.3.3 - arena_bin_nonfull_run_get
     3.3.4 - small run alloc
     3.3.5 - chunk alloc
   3.4 - small allocation (tcache)
   3.5 - large allocation
   3.6 - huge allocation     
4 - Deallocation
   4.1 - Overview
   4.2 - arena_dalloc_bin
   4.3 - small run dalloc
   4.4 - arena purge    
   4.5 - arena chunk dalloc
   4.6 - large/huge dalloc
5 - 總結: 與Dl的對比
附: 快速調試Jemalloc

 

 




--[ 1 - 簡介

Jemalloc最初是Jason Evans為FreeBSD開發的新一代記憶體分配器, 用來替代原來的
phkmalloc, 最早投入使用是在2005年. 到目前為止, 除了原版Je, 還有很多變種
被用在各種項目里. Google在android5.0里將bionic中的預設分配器從Dl替換為Je,
也是看中了其強大的多核多線程分配能力.

同經典分配器, 如Dlmalloc相比, Je在基本思路和實現上存在明顯的差別. 比如,
Dl在分配策略上傾向於先dss後mmap的方式, 為的是快速向前分配, 但Je則完全相反.
而實現上也放棄了經典的boundary tag. 這些設計犧牲了局部分配速度和回收效率,
但在更大的空間和時間範圍內卻獲得更好的分配效果.

更重要的是, 相對經典分配器, Je最大的優勢還是其強大的多核/多線程分配能力.
以現代電腦硬體架構來說, 最大的瓶頸已經不再是記憶體容量或cpu速度, 而是
多核/多線程下的lock contention. 因為無論核心數量如何多, 通常情況下記憶體
只有一份. 可以說, 如果記憶體足夠大, CPU的核心數量越多, 程式線程數越多,
Je的分配速度越快. 而這一點是經典分配器所無法達到的.

這篇文章基於android5.x中的Jemalloc3.6.0. 最新的版本為4.x, 獲取最新代碼請至,

https://github.com/jemalloc/jemalloc/releases

 

--[ 2 - Basic structures

相對於Dl, Je引入了更多更複雜的分配結構, 如arena, chunk, bin, run, region,
tcache等等. 其中有些類似Dl, 但更多的具有不同含義, 本節將對它們做一一介紹.


----[ 2.1 - Overview

首先, 先給出一個整體的概念. Je對記憶體劃分按照如下由高到低的順序,

1. 記憶體是由一定數量的arenas進行管理.
2. 一個arena被分割成若幹chunks, 後者主要負責記錄bookkeeping.
3. chunk內部又包含著若幹runs, 作為分配小塊記憶體的基本單元.
4. run由pages組成, 最終被劃分成一定數量的regions,
5. 對於small size的分配請求來說, 這些region就相當於user memory.

    Arena #0
+----------------------------------------------------------------------------+
|                                                                            |
|    Chunk #0                             Chunk #1                           |
|  +---------------------------------+  +---------------------------------+  |
|  |                                 |  |                                 |  |
|  |   Run #0          Run #1        |  |   Run #0          Run #1        |  |
|  | +-------------+ +-------------+ |  | +-------------+ +-------------+ |  |
|  | |             | |             | |  | |             | |             | |  |
|  | |   Page      | |   Page      | |  | |   Page      | |   Page      | |  |
|  | | +---------+ | | +---------+ | |  | | +---------+ | | +---------+ | |  |
|  | | |         | | | |         | | |  | | |         | | | |         | | |  |
|  | | | Regions | | | | Regions | | |  | | | Regions | | | | Regions | | |  |
|  | | |[] [] [] | | | |[] [] [] | | |  | | |[] [] [] | | | |[] [] [] | | |  |
|  | | |         | | | |         | | |  | | |         | | | |         | | |  |
|  | | +---------+ | | +---------+ | |  | | +---------+ | | +---------+ | |  |  
|  | |             | |             | |  | |             | |             | |  |
|  | |   Page      | |   Page      | |  | |   Page      | |   Page      | |  |
|  | | +---------+ | | +---------+ | |  | | +---------+ | | +---------+ | |  |
|  | | |         | | | |         | | |  | | |         | | | |         | | |  |
|  | | | ...     | | | | ...     | | |  | | | ...     | | | | ...     | | |  |
|  | | +---------+ | | +---------+ | |  | | +---------+ | | +---------+ | |  |
|  | +-------------+ +-------------+ |  | +-------------+ +-------------+ |  |
|  +---------------------------------+  +---------------------------------+  |
+----------------------------------------------------------------------------+

 



----[ 2.2 - Arena (arena_t)

如前所述, Arena是Je中最大或者說最頂層的基礎結構. 這個概念其實上是針對"對稱
多處理機(SMP)"產生的. 在SMP中, 導致性能劣化的一個重要原因在於"false sharing"
導致cache-line失效.

為瞭解決cache-line共用問題, 同時保證更少的內部碎片(internal fragmentation),
Je使用了arena.


------[ 2.2.1 - CPU Cache-Line

現代處理器為瞭解決記憶體匯流排吞吐量的瓶頸使用了內部cache技術. 儘管cache的
工作機制很複雜, 且對外透明, 但在編程上, 還是有必要瞭解cache的基本性質.

Cache是嵌入到cpu內部的一組SRAM, 速度是主存的N倍, 但造價較高, 因此
一般容量很小. 有些cpu設計了容量逐級逐漸增大的多級cache, 但速度逐級遞減.
多級處理更複雜, 但原理類似, 為了簡化, 僅討論L1 data cache.

cache同主存進行數據交換有一個最小粒度, 稱為cache-line, 通常這個值為64. 例如
在一個ILP32的機器上, 一次cache交換可以讀寫連續16個int型數據. 因此當訪問數組
#0元素時, 後面15個元素也被同時"免費"讀到了cache中, 這對於數組的連續訪問是
非常有利的.

然而這種免費載入不總是會帶來好處, 有時候甚至起到反效果, 所謂"false sharing".
試想兩個線程A和B分別執行在不同的cpu核心中並分別操作各自上下文中的變數x和y.
如果因為某種原因(比如x, y可能位於同一個class內部, 或者分別是數組中的兩個相鄰
元素), 兩者位於相同的cache-line中, 則在兩個core的L1 cache里都存在x和y的副本.
倘若線程A修改了x的值, 就會導致在B中的x與A中看到的不一致. 儘管這個變數x對B可能
毫無用處, 但cpu為了保證前後的正確和一致性, 只能判定core #1的cache失效. 因此
core #0必須將cache-line回寫到主存, 然後core #1再重新載入cache-line, 反之亦然.
如果恰好兩個線程交替操作同一cache-line中的數據, 將對cache將造成極大的損害,
導致嚴重的性能退化.

+-----------------------+        +-----------------------+
| core #0               |        | core #1               |
|                       |        |                       |
|  +----------+         |        |  +----------+         |
|  | ThreadA  |         |        |  | ThreadB  |         |
|  +----------+         |        |  +----------+         |
|        |              |        |        |              |
|    +---+              |        |        |              |
|    |                  |        |        |              |
|    v        D-cache   |        |        v     D-cache  |
|  +-----------------+  |        |  +-----------------+  |
|  | x'| y | ... ... | <---+  +---> | x | y'| ... ... |  |
|  |-----------------|  |  |  |  |  |-----------------|  |
|  |    ... ...      |  |  |  |  |  |    ... ...      |  |
|  |    ... ...      |  |  |  |  |  |    ... ...      |  |
|  |    ... ...      |  |  |  |  |  |    ... ...      |  |
|  +-----------------+  |  |  |  |  +-----------------+  |
+-----------------------+  |  |  +-----------------------+
                           |  |  
                    +------+  |
                    |         |
                    v         v
   memory   +-----------------------------
            | ... | x | y |     ... ...     
            +-----------------------------

      
說到底, 從程式的角度看, 變數是獨立的地址單元, 但在CPU看來則是以cache-line為
整體的單元. 單獨的變數競爭可以在代碼中增加同步來解決, 而cache-line的競爭是
透明的, 不可控的, 只能被動由CPU仲裁. 這種觀察角度和處理方式的區別, 正是false
sharing的根源.


------[ 2.2.2 - Arena原理

回到memory allocator的話題上. 對於一個多線程+多CPU核心的運行環境, 傳統
分配器中大量開銷被浪費在lock contention和false sharing上, 隨著線程數量
和核心數量增多, 這種分配壓力將越來越大.

針對多線程, 一種解決方法是將一把global lock分散成很多與線程相關的lock.
而針對多核心, 則要儘量把不同線程下分配的記憶體隔離開, 避免不同線程使用同
一個cache-line的情況. 按照上面的思路, 一個較好的實現方式就是引入arena.

+---------+     +---------+     +---------+     +---------+     +---------+
| threadA |     | threadB |     | threadC |     | threadD |     | threadE |     
+---------+     +---------+     +---------+     +---------+     +---------+
     |               |               |               |               |
     |               +---------------|---------------|---------+     |
     +------------------+    +-------+        +------+         |     |
      +-----------------|----|----------------|----------------|-----+  
      |                 |    |                |                |
      v                 v    v                v                v
+----------+        +----------+        +----------+        +----------+     
|          |        |          |        |          |        |          |
| Arena #0 |        | Arena #1 |        | Arena #2 |        | Arena #3 |
|          |        |          |        |          |        |          |
+----------+        +----------+        +----------+        +----------+

 


Je將記憶體劃分成若幹數量的arenas, 線程最終會與某一個arena綁定. 比如上圖中的
threadA和B就分別綁定到arena #1和#3上. 由於兩個arena在地址空間上幾乎不存在任何
聯繫, 就可以在無鎖的狀態下完成分配. 同樣由於空間不連續, 落到同一個cache-line
中的幾率也很小, 保證了各自獨立.

由於arena的數量有限, 因此不能保證所有線程都能獨占arena, 比如, 圖中threadA和C
就都綁定到了arena1上. 分享同一個arena的所有線程, 由該arena內部的lock保持同步.

Je將arena保存到一個數組中, 該數組全局記錄了所有arenas,

arena_t            **arenas;

事實上, 該數組是動態分配的, arenas僅僅是一個數組指針. 預設情況下arenas數組的
長度與如下變數相關,

unsigned    narenas_total;
unsigned    narenas_auto;
size_t        opt_narenas = 0;

 


而它們又與當前cpu核心數量相關. 核心數量記錄在另外一個全局變數ncpus里,

unsigned    ncpus;

如果ncpus等於1, 則有且僅有一個arena, 如果大於1, 則預設arenas的數量為ncpus的四倍.
即雙核下8個arena, 四核下16個arena, 依此類推.

(gdb) p ncpus
$20 = 4
(gdb) p narenas_total
$21 = 16

 

 

jemalloc變體很多, 不同變體對arenas的數量有所調整, 比如firefox中arena固定為1,
而android被限定為最大不超過2. 這個限制被寫到android jemalloc的mk文件中.


------[ 2.2.3 - choose_arena

最早引入arena並非由Je首創, 但早期線程與arena綁定是通過hash線程id實現的, 相對
來說隨機性比較強. Je改進了綁定的演算法, 使之更加科學合理.

Je中線程與arena綁定由函數choose_arena完成, 被綁定的arena記錄在該線程的tls中,

JEMALLOC_INLINE arena_t *
choose_arena(arena_t *arena)
{
    ......    
    // xf: 通常情況下線程所綁定的arena記錄在arenas_tls中
    if ((ret = *arenas_tsd_get()) == NULL) {
        // xf: 如果當前thread未綁定arena, 則為其指定一個, 並保存到tls
        ret = choose_arena_hard();
    }

    return (ret);
}

 

 

初次搜索arenas_tsd_get可能找不到該函數在何處被定義. 實際上, Je使用了一組巨集,
來生成一個函數族, 以達到類似函數模板的目的. tsd相關的函數族被定義在tsd.h中.

1. malloc_tsd_protos - 定義了函數聲明, 包括初始化函數boot, get/set函數
2. malloc_tsd_externs - 定義變數聲明, 包括tls, 初始化標誌等等
3. malloc_tsd_data - tls變數定義
4. malloc_tsd_funcs - 定義了1中聲明函數的實現.

與arena tsd相關的函數和變數聲明如下,

malloc_tsd_protos(JEMALLOC_ATTR(unused), arenas, arena_t *)
malloc_tsd_externs(arenas, arena_t *)
malloc_tsd_data(, arenas, arena_t *, NULL)
malloc_tsd_funcs(JEMALLOC_ALWAYS_INLINE, arenas, arena_t *, NULL, arenas_cleanup)

 


當線程還未與任何arena綁定時, 會進一步通過choose_arena_hard尋找一個合適的arena
進行綁定. Je會遍歷arenas數組, 並按照優先順序由高到低的順序挑選,

1. 如果找到當前線程綁定數為0的arena, 則優先使用它.
2. 如果當前已初始化arena中沒有線程綁定數為0的, 則優先使用剩餘空的數組位置
   構造一個新的arena. 需要說明的是, arenas數組遵循lazy create原則, 初始狀態
   整個數組只有0號元素是被初始化的, 其他的slot位置都是null指針. 因此隨著新的
   線程不斷創造出來, arena數組也被逐漸填滿.
3. 如果1,2兩條都不滿足, 則選擇當前綁定數最小的, 且slot位置更靠前的一個arena.

arena_t * choose_arena_hard(void)
{
    ......
    if (narenas_auto > 1) {
        ......
        first_null = narenas_auto;
        // xf: 迴圈遍歷所有arenas, 找到綁定thread數量最小的arena, 並記錄
        // first_null索引值
        for (i = 1; i < narenas_auto; i++) {
            if (arenas[i] != NULL) {
                if (arenas[i]->nthreads <
                    arenas[choose]->nthreads)
                    choose = i;
            } else if (first_null == narenas_auto) {
                first_null = i;
            }
        }

        // xf: 若選定的arena綁定thread為0, 或者當前arena數組中已滿, 則返回
        // 被選中的arena
        if (arenas[choose]->nthreads == 0
            || first_null == narenas_auto) {
            ret = arenas[choose];
        } else {
            // xf: 否則構造並初始化一個新的arena
            ret = arenas_extend(first_null);
        }
        ......
    } else {
        // xf: 若不存在多於一個arena(單核cpu或人為強制設定), 則返回唯一的
        // 0號arena
        ret = arenas[0];
        ......
    }

    // xf: 將已綁定的arena設置到tsd中
    arenas_tsd_set(&ret);

    return (ret);
}

 

 


對比早期的綁定方式, Je的演算法顯然更加公平, 儘可能的讓各個cpu核心平分當前線程,
平衡負載.


------[ 2.2.4 - Arena結構

struct arena_s {
    unsigned        ind;        
    unsigned        nthreads;   
    malloc_mutex_t        lock;
    arena_stats_t        stats;  
    ql_head(tcache_t)    tcache_ql;
    uint64_t        prof_accumbytes;
    dss_prec_t        dss_prec;   
    arena_chunk_tree_t    chunks_dirty;
    arena_chunk_t        *spare;
    size_t            nactive;
    size_t            ndirty;
    size_t            npurgatory;
    arena_avail_tree_t    runs_avail;
    chunk_alloc_t        *chunk_alloc;
    chunk_dalloc_t        *chunk_dalloc;
    arena_bin_t        bins[NBINS];
};

 

 

ind: 在arenas數組中的索引值.

nthreads: 當前綁定的線程數.

lock: 局部arena lock, 取代傳統分配器的global lock.
      一般地, 如下操作需要arena lock同步,
      1. 線程綁定, 需要修改nthreads
      2. new chunk alloc
      3. new run alloc
      
stats: 全局統計, 需要打開統計功能.

tcache_ql: ring queue, 註冊所有綁定線程的tcache, 作為統計用途, 需要打開統計功能.

dss_prec: 代表當前chunk alloc時對系統記憶體的使用策略, 分為幾種情況,     

          typedef enum {
            dss_prec_disabled  = 0,
            dss_prec_primary   = 1,
            dss_prec_secondary = 2,
            dss_prec_limit     = 3
          } dss_prec_t;

 

          第一個代表禁止使用系統DSS, 後兩種代表是否優先選用DSS. 如果使用
          primary, 則本著先dss->mmap的順序, 否則按照先mmap->dss. 預設使用
          dss_prec_secondary.

chunks_dirty: rb tree, 代表所有包含dirty page的chunk集合. 後面在chunk中會
              詳細介紹.

spare: 是一個緩存變數, 記錄最近一次被釋放的chunk. 當arena收到一個新的chunk
       alloc請求時, 會優先從spare中開始查找, 由此提高頻繁分配釋放時, 可能
       導致內部chunk利用率下降的情況.

runs_avail: rb tree, 記錄所有未被分配的runs, 用來在分配new run時尋找合適的
            available run. 一般作為alloc run時的倉庫.

chunk_alloc/chunk_dalloc: 用戶可定製的chunk分配/釋放函數, Je提供了預設的版本,
                          chunk_alloc_default/chunk_dalloc_default

bins: bins數組, 記錄不同class size可用free regions的分配信息, 後面會詳細介紹.


----[ 2.3 - Chunk (arena_chunk_t)

chunk是僅次於arena的次級記憶體結構. 如果有瞭解過Dlmalloc, 就會知道在Dl中同樣
定義了名為'chunk'的基礎結構. 但這個概念在兩個分配器中含義完全不同, Dl中的
chunk指代最低級分配單元, 而Je中則是一個較大的記憶體區域.


------[ 2.3.1 - overview

從前面arena的數據結構可以發現, 它是一個非常抽象的概念, 其大小也不代表實際的
記憶體分配量. 原始的記憶體數據既非掛載在arena外部, 也並沒有通過內部指針引用,
而是記錄在chunk中. 按照一般的思路, chunk包含原始記憶體數據, 又從屬於arena,
因此後者應該會有一個數組之類的結構以記錄所有chunk信息. 但事實上同樣找不到
這樣的記錄. 那Je又如何獲得chunk指針呢?

所謂的chunk結構, 只是整個chunk的一個header, bookkeeping以及user memory都掛在
header外面. 另外Je對chunk又做了規定, 預設每個chunk大小為4MB, 同時還必須對齊
到4MB的邊界上.

#define    LG_CHUNK_DEFAULT    22

 


這個巨集定義了chunk的大小. 註意到首碼'LG_', 代表log即指數部分.
Je中所有該首碼的代碼都是這個含義, 便於通過bit操作進行快速的運算.

有了上述規定, 獲得chunk就變得幾乎沒有代價. 因為返回給user程式的記憶體地址肯定
屬於某個chunk, 而該chunk header對齊到4M邊界上, 且不可能超過4M大小, 所以只需要
對該地址做一個下對齊就得到chunk指針, 如下,

#define    CHUNK_ADDR2BASE(a)                        \
    ((void *)((uintptr_t)(a) & ~chunksize_mask))

 
計算相對於chunk header的偏移量,

#define    CHUNK_ADDR2OFFSET(a)                        \
    ((size_t)((uintptr_t)(a) & chunksize_mask))

 
以及上對齊到chunk邊界的計算,

#define    CHUNK_CEILING(s)                        \
    (((s) + chunksize_mask) & ~chunksize_mask)

 

用圖來表示如下,   

   chunk_ptr(4M aligned)                memory for user
    |                                   |
    v                                   v
    +--------------+--------------------------------------------
    | chunk header |        ... ...     | region |   ... ...     
    +--------------+--------------------------------------------
    |<------------- offset ------------>|

 


------[ 2.3.2 - Chunk結構

struct arena_chunk_s {
    arena_t            *arena;
    rb_node(arena_chunk_t)    dirty_link;
    size_t            ndirty;
    size_t            nruns_avail;
    size_t            nruns_adjac;
    arena_chunk_map_t    map[1];
}

 

arena: chunk屬於哪個arena

dirty_link: 用於rb tree的鏈接節點. 如果某個chunk內部含有任何dirty page,
            就會被掛載到arena中的chunks_dirty tree上.

ndirty: 內部dirty page數量.

nruns_avail: 內部available runs數量.

nruns_adjac: available runs又分為dirty和clean兩種, 相鄰的兩種run是無法合併的,
             除非其中的dirty runs通過purge才可以. 該數值記錄的就是可以通過
             purge合併的run數量.

map: 動態數組, 每一項對應chunk中的一個page狀態(不包含header即map本身的占用).
     chunk(包括內部的run)都是由page組成的. page又分為unallocated, small,
     large三種.
     unallocated指的那些還未建立run的page.
     small/large分別指代該page所屬run的類型是small/large run.
     這些page的分配狀態, 屬性, 偏移量, 及其他的標記信息等等, 都記錄在
     arena_chunk_map_t中.

|<--------- map_bias ----------->|
| page | page |  ... ...  | page |
+-----------------------------------------------------------------------+
| chunk_header |    chunk map    | page #0  | page #1  | ... | page #n  |
|    ... ...   | [0] [1] ... [n] |          |          |     |          |
+-----------------|---|-------|-----------------------------------------+
                  |   |       |       ^          ^                 ^
                  +---|-------|-------+          |                 |
                      +-------|------------------+                 |
                              + -----------------------------------+

 
至於由chunk header和chunk map占用的page數量, 保存在map_bias變數中.
該變數是Je在arena boot時通過迭代演算法預先計算好的, 所有chunk都是相同的.
迭代方法如下,

1. 第一次迭代初始map_bias等於0, 計算最大可能大小, 即
   header_size + chunk_npages * map_size
   獲得header+map需要的page數量, 結果肯定高於最終的值.
2. 第二次將之前計算的map_bias迭代回去, 將最大page數減去map_bias數, 重新計算
   header+map大小, 由於第一次迭代map_bias過大, 第二次迭代必定小於最終結果.
3. 第三次再將map_bias迭代回去, 得到最終大於第二次且小於第一次的計算結果.

相關代碼如下,

void
arena_boot(void)
{
    ......
    map_bias = 0;
    for (i = 0; i < 3; i++) {
        header_size = offsetof(arena_chunk_t, map) +
            (sizeof(arena_chunk_map_t) * (chunk_npages-map_bias));
        map_bias = (header_size >> LG_PAGE) + ((header_size & PAGE_MASK)
            != 0);
    }
    ......
}

 


------[ 2.3.3 - chunk map (arena_chunk_map_t)

chunk記錄page狀態的結構為arena_chunk_map_t, 為了節省空間, 使用了bit壓縮存儲信息.

struct arena_chunk_map_s {
#ifndef JEMALLOC_PROF
    union {
#endif
    union {
        rb_node(arena_chunk_map_t)    rb_link;
        ql_elm(arena_chunk_map_t)    ql_link;
    }                u;
    prof_ctx_t            *prof_ctx;
#ifndef JEMALLOC_PROF
    };
#endif
    size_t                bits;
}

 
chunk map內部包含兩個link node, 分別可以掛載到rb tree或環形隊列上, 同時
為了節省空間又使用了union. 由於run本身也是由連續page組成的, 因此chunk map
除了記錄page狀態之外, 還負責run的基址檢索.

舉例來說, Je會把所有已分配run記錄在內部rb tree上以快速檢索, 實際地操作是
將該run中第一個page對應的chunk_map作為rb node掛載到tree上. 檢索時也是先
找出將相應的chunk map, 再進行地址轉換得到run的基址.

按照通常的設計思路, 我們可能會把run指針作為節點直接保存到rb tree中. 但Je中
的設計明顯要更複雜. 究其原因, 如果把link node放到run中, 後果是bookkeeping和
user memory將混淆在一起, 這對於分配器的安全性是很不利的. 包括Dl在內的傳統
分配器都具有這樣的缺陷. 而如果單獨用link node記錄run, 又會造成空間浪費.
正因為Je中無論是chunk還是run都是連續page組成, 所以用首個page對應的chunk map
就能同時代表該run的基址.

Je中通常用mapelm換算出pageind, 再將pageind << LG_PAGE + chunk_base, 就能得到
run指針, 代碼如下,

arena_chunk_t *run_chunk = CHUNK_ADDR2BASE(mapelm);
size_t pageind = arena_mapelm_to_pageind(mapelm);
run = (arena_run_t *)((uintptr_t)run_chunk + (pageind <<
    LG_PAGE));

JEMALLOC_INLINE_C size_t
arena_mapelm_to_pageind(arena_chunk_map_t *mapelm)
{
    uintptr_t map_offset =
        CHUNK_ADDR2OFFSET(mapelm) - offsetof(arena_chunk_t, map);

    return ((map_offset / sizeof(arena_chunk_map_t)) + map_bias);
}

 
chunk map對page狀態描述都壓縮記錄到bits中, 由於內容較多, 直接引用Je代碼
中的註釋,

  下麵是一個假想的ILP32系統下的bits layout,

  ???????? ???????? ????nnnn nnnndula

  "?"的部分分三種情況, 分別對應unallocated, small和large.
  ? : Unallocated: 首尾page寫入該run的地址, 而內部page則不做要求.
      Small: 全部是page的偏移量.
      Large: 首page是run size, 後續的page不做要求.
  n : 對於small run指其所在bin的index, 對large run寫入BININD_INVALID.
  d : dirty?
  u : unzeroed?
  l : large?
  a : allocated?

  下麵是對三種類型的run page做的舉例,

  p : run page offset
  s : run size
  n : binind for size class; large objects set these to BININD_INVALID
  x : don't care
  - : 0
  + : 1
  [DULA] : bit set
  [dula] : bit unset

  Unallocated (clean):
    ssssssss ssssssss ssss++++ ++++du-a
    xxxxxxxx xxxxxxxx xxxxxxxx xxxx-Uxx
    ssssssss ssssssss ssss++++ ++++dU-a

  Unallocated (dirty):
    ssssssss ssssssss ssss++++ ++++D--a
    xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx
    ssssssss ssssssss ssss++++ ++++D--a

  Small:      
    pppppppp pppppppp ppppnnnn nnnnd--A
    pppppppp pppppppp ppppnnnn nnnn---A
    pppppppp pppppppp ppppnnnn nnnnd--A
    
    Small page需要註意的是, 這裡代表的p並非是一個固定值, 而是該page相對於
    其所在run的第一個page的偏移量, 比如可能是這樣,
    00000000 00000000 0000nnnn nnnnd--A
    00000000 00000000 0001nnnn nnnn---A
    00000000 00000000 0010nnnn nnnn---A
    00000000 00000000 0011nnnn nnnn---A
    ...
    00000000 00000001 1010nnnn nnnnd--A

  Large:
    ssssssss ssssssss ssss++++ ++++D-LA
    xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx
    -------- -------- ----++++ ++++D-LA

  Large (sampled, size <= PAGE):
    ssssssss ssssssss ssssnnnn nnnnD-LA

  Large (not sampled, size == PAGE):
    ssssssss ssssssss ssss++++ ++++D-LA

為了提取/設置map bits內部的信息, Je提供了一組函數, 這裡列舉兩個最基本的,
剩下的都是讀取mapbits後做一些位運算而已,

讀取mapbits,

JEMALLOC_ALWAYS_INLINE size_t
arena_mapbits_get(arena_chunk_t *chunk, size_t pageind)
{
    return (arena_mapbitsp_read(arena_mapbitsp_get(chunk, pageind)));
}

 


根據pageind獲取對應的chunk map,

JEMALLOC_ALWAYS_INLINE arena_chunk_map_t *
arena_mapp_get(arena_chunk_t *chunk, size_t pageind)
{
    ......
    return (&chunk->map[pageind-map_bias]);
}

 

 


----[ 2.4 - Run (arena_run_t)

如同在2.1節所述, 在Je中run才是真正負責分配的主體(前提是對small region來說).
run的大小對齊到page size上, 並且在內部劃分成大小相同的region. 當有外部分配
請求時, run就會從內部挑選一個free region返回. 可以認為run就是small region倉庫.


------[ 2.4.1 - Run結構

struct arena_run_s {
    arena_bin_t    *bin;
    uint32_t    nextind;
    unsigned    nfree;
};

 


run的結構非常簡單, 但同chunk類似, 所謂的arena_run_t不過是整個run的header部分.

bin:     與該run相關聯的bin. 每個run都有其所屬的bin, 詳細內容在之後介紹.
nextind: 記錄下一個clean region的索引.
nfree:   記錄當前空閑region數量.

除了header部分之外, run的真實layout如下,

               /--------------------\
               | arena_run_t header |
               | ...                |
 bitmap_offset | bitmap             |
               | ...                |
               |--------------------|
               | redzone            |
   reg0_offset | region 0           |
               | redzone            |
               |--------------------| \
               | redzone            | |
               | region 1           |  > reg_interval
               | redzone            | /
               |--------------------|
               | ...                |
               | ...                |
               | ...                |
               |--------------------|
               | redzone            |
               | region nregs-1     |
               | redzone            |
               |--------------------|
               | alignment pad?     |
               \--------------------/

 

              
正如chunk通過chunk map記錄內部所有page狀態一樣, run通過在header後掛載
bitmap來記錄其內部的region狀態. bitmap之後是regions區域. 內部region
大小相等, 且在前後都有redzone保護(需要在設置里打開redzone選項).

簡單來說, run就是通過查詢bitmap來找到可用的region. 而傳統分配器由於使用
boundary tag, 空閑region一般是被雙向鏈表管理的. 相比之下, 傳統方式查找
速度更快, 也更簡單. 缺點之前也提到過, 安全和穩定性都存在缺陷. 從這一點
可以看到, Je在設計思路上將bookkeeping和user memory分離是貫穿始終的原則,
更甚於對性能的影響(事實上這點影響在併發條件下被大大賺回來了).


------[ 2.4.2 - size classes

記憶體分配器對內部管理的region往往按照某種特殊規律來分配. 比如Dl將記憶體劃分成
small和large兩種類型. small類型從8位元組開始每8個位元組為一個分割直至256位元組.
而large類型則從256位元組開始, 掛載到dst上. 這種劃分方式有助於分配器對記憶體進行
有效的管理和控制, 讓已分配的記憶體更加緊實(tightly packed), 以降低外部碎片率.

Je進一步優化了分配效率. 採用了類似於"二分伙伴(Binary Buddy)演算法"的分配方式.
在Je中將不同大小的類型稱為size class.

在Je源碼的size_classes.h文件中, 定義了不同體系架構下的region size. 該文件
實際是通過名為size_classes.sh的shell script自動生成的. script按照四種不同
量綱定義來區分各個體系平臺的區別, 然後將它們做排列組合, 就可以相容各個體系.
這四種量綱分別是,

LG_SIZEOF_PTR: 代表指針長度, ILP32下是2, LP64則是3.

LG_QUANTUM: 量子, binary buddy分得的最小單位. 除了tiny size, 其他的size
            classes都是quantum的整數倍大小.

LG_TINY_MIN: 是比quantum更小的size class, 且必須對齊到2的指數倍上. 它是Je可
             分配的最小的size class.

LG_PAGE: 就是page大小

根據binary buddy演算法, Je會將記憶體不斷的二平分, 每一份稱作一個group. 同一個
group內又做四等分. 例如, 一個典型的ILP32, tiny等於8byte, quantum為16byte,
page為4096byte的系統, 其size classes劃分是這樣的,

#if (LG_SIZEOF_PTR == 2 && LG_TINY_MIN == 3 && LG_QUANTUM == 4 && LG_PAGE == 12)
#define    SIZE_CLASSES \
      index, lg_grp, lg_delta, ndelta,  bin, lg_delta_lookup  \
    SC(  0,      3,        3,      0,   yes,        3) \        
                                                       \
    SC(  1,      3,        3,      1,   yes,        3) \        
    SC(  2,      4,        4,      1,   yes,        4) \        
    SC(  3,      4,        4,      2,   yes,        4) \        
    SC(  4,      4,        4,      3,   yes,        4) \        
                                                       \
    SC(  5,      6,        4,      1,   yes,        4) \        
    SC(  6,      6,        4,      2,   yes,        4) \        
    SC(  7,      6,        4,      3,   yes,        4) \        
    SC(  8,      6,        4,      4,   yes,        4) \        
                                                       \
    SC(  9,      7,        5,      1,   yes,        5) \        
    SC( 10,      7,        5,      2,   yes,        5) \        
    SC( 11,      7,        5,      3,   yes,        5) \        
    SC( 12,      7,        5,      4,   yes,        5) \        
                                                       
    ... ...

 


巨集SIZE_CLASSES主要功能就是可以生成幾種類型的table. 而SC則根據不同的情況
被定義成不同的含義. SC傳入的6個參數的含義如下,

index:      在table中的位置
lg_grp:     所在group的指數
lg_delta:   group內偏移量指數
ndelta:     group內偏移數
bin:        是否由bin記錄. small region是記錄在bins中
lg_delta_lookup:    在lookup table中的調用S2B_#的尾數尾碼

因此得到reg_size的計算公式, reg_size = 1 << lg_grp + ndelta << lg_delta
根據該公式, 可以得到region的範圍,

       ┌─────────┬─────────┬───────────────────────────────────────┐
       │Category │ Spacing │ Size                                  │
       ├─────────┼─────────┼───────────────────────────────────────┤
       │         │      lg │ [8]                                   │
       │         ├─────────┼───────────────────────────────────────┤
       │         │      16 │ [16, 32, 48, ..., 128]                │
       │         ├─────────┼───────────────────────────────────────┤
       │         │      32 │ [160, 192, 224, 256]                  │
       │         ├─────────┼───────────────────────────────────────┤
       │Small    │      64 │ [320, 384, 448, 512]                  │
       │         ├─────────┼───────────────────────────────────────┤
       │         │     128 │ [640, 768, 896, 1024]                 │
       │         ├─────────┼───────────────────────────────────────┤
       │         │     256 │ [1280, 1536, 1792, 2048]              │
       │         ├─────────┼───────────────────────────────────────┤
       │         │     512 │ [2560, 3072, 3584]                    │
       ├─────────┼─────────┼───────────────────────────────────────┤
       │Large    │   4 KiB │ [4 KiB, 8 KiB, 12 KiB, ..., 4072 KiB] │
       ├─────────┼─────────┼───────────────────────────────────────┤
       │Huge     │   4 MiB │ [4 MiB, 8 MiB, 12 MiB, ...]           │
       └─────────┴─────────┴───────────────────────────────────────┘

 


除此之外, 在size_classes.h中還定義了一些常量,

tiny bins的數量
#define    NTBINS            1

可以通過lookup table查詢的bins數量
#define    NLBINS            29

small bins的數量
#define    NBINS            28

最大tiny size class的指數
#define    LG_TINY_MAXCLASS    3

最大lookup size class, 也就是NLBINS - 1個bins
#define    LOOKUP_MAXCLASS        ((((size_t)1) << 11) + (((size_t)4) << 9))

最大small size class, 也就是NBINS - 1個bins
#define    SMALL_MAXCLASS        ((((size_t)1) << 11) + (((size_t)3) << 9))


------[ 2.4.3 - size2bin/bin2size

通過SIZE_CLASSES建立的table就是為了在O(1)的時間複雜度內快速進行size2bin或者
bin2size切換. 同樣的技術在Dl中有所體現, 來看Je是如何實現的.

size2bin切換提供了兩種方式, 較快的是通過查詢lookup table, 較慢的是計算得到.
從原理上, 只有small size class需要查找bins, 但可通過lookup查詢的size class
數量要小於整個small size class數量. 因此, 部分size class只能計算得到.
在原始Je中統一隻採用查表法, 但在android版本中可能是考慮減小lookup table
size, 而增加了直接計演算法.

JEMALLOC_ALWAYS_INLINE size_t
small_size2bin(size_t size)
{
    ......
    if (size <= LOOKUP_MAXCLASS)
        return (small_size2bin_lookup(size));
    else
        return (small_size2bin_compute(size));
}

 


小於LOOKUP_MAXCLASS的請求通過small_size2bin_lookup直接查表.
查詢的演算法是這樣的,

size_t ret = ((size_t)(small_size2bin_tab[(size-1) >> LG_TINY_MIN]));

 


也就是說, Je通過一個

 

    f(x) = (x - 1) / 2^LG_TINY_MIN

 

的變換將size映射到lookup table的相應區域. 這個table在gdb中可能是這樣的,

(gdb) p  /d small_size2bin
$6 = {0, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 9, 9, 10, 10, 10, 10,
      11, 11, 11, 11, 12, 12, 12, 12, 13, 13, 13, 13, 13, 13, 13, 13, 14, 14, 14,
      14, 14, 14, 14, 14, 15, 15, 15, 15, 15, 15, 15, 15, 16, 16, 16, 16, 16, <

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 環境說明:Vistual Studio 2013 MVC 4.0 其實關於ASP.NET MVC Area使用的基礎知識可以參考 http://www.cnblogs.com/willick/p/3331519.html 這篇軟文. Global.asax 中的 Application_Start
  • 分類:C#、Android、百度地圖應用; 日期:2016-02-04 一、簡介 百度地圖SDK為廣大開發者開放了OpenGL繪製介面,幫助開發者在地圖上實現更靈活的樣式繪製,豐富地圖使用效果體驗。 二、運行截圖 簡介:介紹如何使用OpenGL在地圖上實現自定義繪製。 詳述: (1)利用OpenGL...
  • .net coreclr 已經發佈RC1版本,安裝方法如下: 1.安裝DNVM,DNVM是.net運行時管理器,負責管理所有版本的.net運行時(.net framework、.net coreclr和Mono)。 C:\coreclr-demo> @powershell -NoProfile -E
  • 分類:C#、Android、百度地圖應用; 日期:2016-02-04 百度全景圖是一種實景地圖服務。為用戶提供城市、街道和其他環境的360度全景圖像,用戶可以通過該服務獲得如臨其境的地圖瀏覽體驗。 本示例演示如何利用百度Android全景SDK v2.2實現全景圖的檢索、顯示和交互功能,以便清晰方...
  • 如果想知道 AngularJs 通過WebAPI 下載Excel。請看下文,這裡僅提供了一種方案。 伺服器端代碼如下: protected HttpResponseMessage GenereateExcelMessage(HttpRequestMessage Request, string fil
  • 分類:C#、Android、百度地圖應用; 日期:2016-02-04 一、簡介 線路規劃支持以下功能: 公交信息查詢:可對公交詳細信息進行查詢; 公交換乘查詢:根據起、終點,查詢策略,進行線路規劃方案; 駕車線路規劃:提供不同策略,規劃駕車路線;(支持設置途經點) 步行路徑檢索:支持步行路徑的規劃...
  • 本文翻譯自《effective modern C++》,由於水平有限,故無法保證翻譯完全正確,歡迎指出錯誤。謝謝! 根據std::move和std::forward不能做什麼來熟悉它們是一個好辦法。std::move沒有move任何東西,std::forward沒有轉發任何東西。在運行期,它們沒有做
  • 數據類型 可以使用BIF type()來查看對象的類型 數字 int float long 布爾(bool) True 機內表示1,機器識別非0 False 機內表示0,機器識別0 空值 None 字元串(str) 移除空格(strip) 分割(split) 長度(len) 列表(list) hel
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...