[轉]PHP的內存管理

     另外, 爲什麼要寫這個呢, 因爲之前並沒有任何資料來介紹PHP內存管理中使用的策略, 數據結構, 或者算法. 而在我們平時開發擴展, 修復PHP的bug的時候, 卻對這一部分的知識需要有一個良好的理解. PHP開發組內的很多朋友也對這塊不是很清楚, 所以我覺得有必要專門寫一下.

     一些基本的概念, 我就不贅述了, 因爲看代碼很容易能看懂, 我這裏就主要介紹幾個看代碼沒那麼容易看懂的點, 爲什麼這麼說呢, 呵呵, 我在寫文章之前, 查找了下已有的資料, 已避免重複功, 其中看到了TIPI項目對這部分的描述, 我只能說, 錯誤很多. 所以, 我想這部分就是看代碼也沒那麼容易看懂的點 :)

     目前, 英文版的介紹也在撰寫中: Zend MM

     Zend Memory Manager, 以下簡稱Zend MM, 是PHP中內存管理的邏輯. 其中有一個關鍵數據結構: zend_mm_heap: 

     Zend MM把內存非爲小塊內存和大塊內存倆種, 區別對待, 對於小塊內存, 這部分是最最常用的, 所以追求高性能. 而對於大塊內存, 則追求的是穩妥, 儘量避免內存浪費.

     所以, 對於小塊內存, PHP還引入了cache機制: 

     Zend MM 希望通過cache儘量做到, 一次定位就能查找分配.

     而一個不容易看懂的點是free_buckets的申明:

     Q: 爲什麼free_buckets數組的長度是ZEND_MM_NUMBER_BUCKET個?

     A: 這是因爲, PHP在這處使用了一個技巧, 用一個定長的數組來存儲ZEND_MM_NUMBER_BUCKET個zend_mm_free_block, 如上圖中紅色框所示. 對於一個沒有被使用的free_buckets的元素, 唯一有用的數據結構就是next_free_block和prev_free_block, 所以, 爲了節省內存, PHP並沒有分配ZEND_MM_NUMBER_BUCKET * sizeof(zend_mm_free_block)大小的內存, 而只是用了ZEND_MM_NUMBER_BUCKET * (sizeof(*next_free_block) + sizeof(*prev_free_block))大小的內存..

     我們來看ZEND_MM_SMALL_FREE_BUCKET宏的定義:

#define ZEND_MM_SMALL_FREE_BUCKET(heap, index) \\
    (zend_mm_free_block*) ((char*)&heap->free_buckets[index * 2] + \\
        sizeof(zend_mm_free_block*) * 2 - \\
        sizeof(zend_mm_small_free_block))

     之後, Zend MM 保證只會使用prev和next倆個指針, 所以不會造成內存讀錯..

     那麼, 第二個不容易看懂的點, 就是PHP對large_free_buckets的管理, 先介紹分配(TIPI項目組對此部分的描述有些含糊不清):

static zend_mm_free_block *zend_mm_search_large_block(zend_mm_heap *heap, size_t true_size)

     large_free_buckets可以說是一個建樹和雙向列表的結合: 

     雙向列表保持着同樣size的內存塊, 而左右孩子(child[0]和child[1])分別代表着鍵值0和1, 這個鍵值是指什麼呢?

     我們來舉個例子, 比如我向PHP申請一個true_size爲0b11010大小的內存, 經過一番步驟以後, 沒有找到合適的內存, PHP進入了zend_mm_search_large_block的邏輯來在large_free_buckets中尋找合適的內存:

    1. 首先, 計算true_size對應的index, 計算方法是, 獲取true_size中最高位1的序號(zend_mm_high_bit), 對應的彙編指令是bsr(此處, TIPI項目錯誤的說明爲: “這個hash函數用來計算size的位數,返回值爲size二進碼中1的個數-1″).

    2. 然後在一個位圖結構中, 判斷是否存在一個大於true_size的可用內存已經存在於large_free_buckets, 如果不存在就返回:

size_t bitmap = heap->large_free_bitmap >> index;
if (bitmap == 0) {
   return NULL;
}

    3. 判斷, free_buckets[index]是否存在可用的內存:

if (UNEXPECTED((bitmap & 1) != 0))

    4. 如果存在, 則從free_buckets[index]開始, 尋找最合適的內存, 步驟如下:

     1. 從free_buckets[index]開始, 如果free_buckets[index]當前的內存大小和true_size相等, 則尋找結束, 成功返回.

     2. 查看true_size的當前最高位, 如果爲1. 則在free_buckets[index]->child[1]下面繼續尋找, 如果free_buckets[index]->child[1]不存在, 則跳出. 如果true_size的當前最高位爲0, 則在free_buckets[index]->child[0]下面繼續尋找, 如果free_buckets[index]->child[0]不存在, 則在free_buckets[index]->child[1]下面尋找最小內存(因爲此時可以保證, 在free_buckets[index]->child[1]下面的內存都是大於true_size的)

     3. 出發點變更爲2中所述的child, 左移一位ture_size.

    5. 如果上述邏輯並沒有找到合適的內存, 則尋找最小的”大塊內存”:

   /* Search for smallest "large" free block */
    best_fit = p = heap->large_free_buckets[index + zend_mm_low_bit(bitmap)];
    while ((p = p->child[p->child[0] != NULL])) {
        if (ZEND_MM_FREE_BLOCK_SIZE(p) < ZEND_MM_FREE_BLOCK_SIZE(best_fit)) {
            best_fit = p;
        }
    }

    注意上面的邏輯, (p = p->child[p->child[0] != NULL]), PHP在儘量尋找最小的內存.

    爲什麼說, large_free_buckets是個鍵樹呢, 從上面的邏輯我們可以看出, PHP把一個size, 按照二進制的01做鍵, 把內存大小信息反應到了鍵樹上, 方便了快速查找.

    另外, 還有一個rest_buckets, 這個結構是個雙向列表, 用來保存一些PHP分配後剩下的內存, 避免無意義的把剩餘內存插入free_buckets帶來的性能問題(此處, TIPI項目錯誤的描述爲: “這是一個只有兩個元素的數組。 而我們常用的插入和查找操作是針對第一個元素,即heap->rest_buckets[0]“).

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章