91超碰碰碰碰久久久久久综合_超碰av人澡人澡人澡人澡人掠_国产黄大片在线观看画质优化_txt小说免费全本

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Python內存管理器怎么實現池化技術

發布時間:2022-05-30 09:34:52 來源:億速云 閱讀:154 作者:iii 欄目:開發技術

這篇文章主要講解了“Python內存管理器怎么實現池化技術”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“Python內存管理器怎么實現池化技術”吧!

前言

Python 中一切皆對象,這些對象的內存都是在運行時動態地在堆中進行分配的,就連 Python 虛擬機使用的棧也是在堆上模擬的。既然一切皆對象,那么在 Python 程序運行過程中對象的創建和釋放就很頻繁了,而每次都用 malloc() 和 free() 去向操作系統申請內存或釋放內存就會對性能造成影響,畢竟這些函數最終都要發生系統調用引起上下文的切換。

其實核心就是池化技術,一次性向操作系統申請一批連續的內存空間,每次需要創建對象的時候就在這批空間內找到空閑的內存塊進行分配,對象釋放的時候就將對應的內存塊標記為空閑,這樣就避免了每次都向操作系統申請和釋放內存,只要程序中總的對象內存空間穩定,Python 向操作系統申請和釋放內存的頻率就會很低。這種方案是不是很熟悉,數據庫連接池也是類似的思路。一般后端應用程序也是提前跟數據庫建立多個連接,每次執行 SQL 的時候就從中找一個可用的連接與數據庫進行交互,SQL 完成的時候就將連接交還給連接池,如果某個連接長時間未被使用,連接池就會將其釋放掉。本質上,這些都是用空間換時間,消耗一些不算太大的內存,降低諸如內存申請和 TCP 建立連接等耗時操作的頻率,提高程序整體的運行速度。

內存層次結構

Python 內存管理器對內存進行了分層,從大到小分別為 arena、pool 和 block。arena 是內存管理器直接調用 malloc() 或 calloc() 向操作系統申請的一大塊內存,Python 中對象的創建和釋放都是在 arena 中進行分配和回收。在 arena 內部又分成了多個 pool,每個 pool 內又分成了多個大小相等的 block,每次分配內存的時候都是從某個 pool 中選擇一塊可用的 block 返回。每個 pool 內的 block 的大小是相等的,不同 pool 的 block 大小可以不等。

Python內存管理器怎么實現池化技術

arena、pool 和 block 的大小在 32 位機器和 64 位機器上有所不同,block 的大小必須是 ALIGNMENT 的倍數,并且最大為 512 字節,下表列出了不同機器上各種內存的大小。

 32 位機器64 位機器
arena size256 KB1 MB
pool size4 KB16 KB
ALIGNMENT8 B16 B

以 64 位機器為例,所有可能的 block 的大小為 16、32、48 … 496、512,每個大小都對應一個分級(size class),從小到大依次為0、1、2 … 30、31。每次分配內存的時候就是找到一個不小于請求大小的最小的空閑 block。對 block 的大小進行分級是為了適應不同大小的內存請求,減少內存碎片的產生,提高 arena 的利用率。

內存管理邏輯

了解了 arena、pool 和 block 的概念后就可以描述內存分配的邏輯了,假如需要的內存大小為 n 字節

  • 如果 n > 512,回退為 malloc(),因為 block 最大為 512 字節

  • 否則計算出不小于 n 的最小的 block size,比如 n=105,在 64 位機器上最小的 block size 為 112

  • 找到對應 2 中 block size 的 pool,從中分配一個 block。如果沒有可用的 pool 就從可用的 arena 中分配一個 pool,如果沒有可用的 arena 就用 malloc() 向操作系統申請一塊新的 arena

釋放內存的邏輯如下

  • 先判斷要釋放的內存是不是由 Python 內存管理器分配的,如果不是直接返回

  • 找到要釋放的內存對應的 block 和 pool,并將 block 歸還給 pool,留給下次分配使用

  • 如果釋放的 block 所在的 arena 中除了自己之外其他的都是空閑的,那么在 block 歸還之后整個 arena 都是空閑的,就可以將 arena 用 free() 釋放掉還給操作系統

Python 中的對象一般都不大,并且生命周期很短,所以 arena 一旦申請之后,對象的分配和釋放大部分情況下都是在 arena 中進行的,提高了效率。
上文已經將 Python 內存管理器的核心邏輯描述清楚了,只不過有一些細節的問題還沒解決,比如內存分配的時候怎么根據 block size 找到對應的 pool,這些 pool 之間怎么關聯起來的,內存釋放的時候又是怎么判斷要釋放的內存是不是 Python 內存管理器分配的,等等。下面結合源碼將內存分配和釋放的邏輯詳細展開。

先介紹 arena 和 pool 的內存布局和對應的數據結構,然后再具體分析 pymalloc_alloc() 和 pymalloc_free() 的邏輯,以 64 位機器為例介紹。

內存布局及對應的數據結構

Arena

Python內存管理器怎么實現池化技術

arena 為 1 MB,pool 為 16 KB,pool 在 arena 中是相鄰的,一個 arena 中最多有 1 MB / 16 KB = 64 個 pool。Python 內存管理器會將 arena 中第一個 pool 的首地址跟 POOL_SIZE 對齊,這樣每個 pool 的首地址都是 POOL_SIZE 的整數倍,給定任意內存地址都可以很方便的計算出其所在 pool 的首地址,這個特性在內存釋放的時候會用到。POOL_SIZE 在 32 位機器上是 4 KB,在 64 位機器上是 16 KB,這樣做還有另外一個好處就是讓每個 pool 正好落在一個或多個物理頁中,提高了訪存效率。上圖中的灰色內存塊就是為了對齊而丟棄掉的,如果 malloc() 分配的內存首地址恰好對齊了,那么 pool 的數量就是 64,否則就是 63。當然 arena 不是一開始就將全部的 pool 都劃分出來,而是在沒有可用的 pool 的時候才會去新劃分一個,當所有的 pool 全部劃分之后布局如上圖所示。

每個 arena 都由結構體 struct arena_object 來表示,但不是所有 struct arena_object 都有對應的 arena,因為 arena 釋放之后對應的 struct arena_object 還保留著,這些沒有對應 arena 的 struct arena_object 存放在單鏈表 unused_arena_objects 中,在下次分配 arena 時可以拿來使用。如果 struct arena_object 有對應的 arena,并且 arena 中有可以分配的 pool,那么 struct arena_object 會存放在 usable_arenas 這個雙向鏈表中,同時,所有的 struct arena_object 無論有沒有對應的 arena 都存在數組 arenas 中。usable_arenas 中 arena 是按照其包含的空閑 pool 的數量從小到大排序的,這么排序是為了讓已經使用了更多內存的 arena 在下次分配 pool 的時候優先被使用,那么在釋放內存的時候排在后面的那些擁有更多空閑內存的 arena 就有更大可能變成完全空閑狀態,從而被釋放掉將其內存空間歸還給操作系統,降低整體的內存消耗。

struct arena_object 的結構及各字段含義如下

struct arena_object {
    uintptr_t address; // 指向 arena 的起始地址,如果當前 arena_object 沒有對應的 arena 內存則 address = 0
    block* pool_address; // pool 需要初始化之后才能使用,pool_address 指向的地址可以用來初始化一個 pool 用于分配
    int nfreepools; // arena 中目前可以用來分配的 pool 的數量
    uint ntotalpools; // arena 中 pool 的總數量,64 或 63
    struct pool_header* freepools; // arena 中可以分配的 pool 構成一個單鏈表,freepools 指針是單鏈表的第一個節點
    struct arena_object* nextarena; // 在 usable_arenas 或 unused_arena_objects 指向下一個節點
    struct arena_object* prevarena; // 在 usable_arenas 中指向上一個節點
}

Pool

Python內存管理器怎么實現池化技術

pool 的內部等分成多個大小相等的 block,與 arena 一樣,也有一個數據結構 struct pool_header 用來表示 pool。與 arena 不同的是,struct pool_header 位于 pool 的內部,在最開始的一段內存中,緊接之后的是第一個 block,為了讓每個 block 的地址都能對齊機器訪問內存的步長,可能需要在 struct pool_header 和第一個 block 之間做一些 padding,圖中灰色部分所示。這部分 padding 不一定存在,在 64 位機器上 sizeof(struct pool_header) 為 48 字節,本來就已經對齊了,后面就直接跟第一個 block,中間沒有 padding。即使如此,pool 最后的一小塊內存也可能用不上,上圖中下面的灰色部分所示,因為每個 pool 中 block 大小是相等的,假設 block 為 64 字節,一個 pool 中可以分出 255 個 block,前面 48 字節存儲 struct pool_header,后面 16 字節用不上,當然如果 block 大小為 48 字節或 16 字節那么整個 pool 就會被完全利用上。同 arena 一樣,pool 一開始不是把所有的 block 全部劃分出來,而是在沒有可用 block 的時候才回去新劃分一個,在所有的 block 全部劃分之后 pool 的布局如上圖所示。

接下來看看 struct pool_header 的結構

struct pool_header {
    union { block *_padding;
            uint count; } ref; // 當前 pool 中已經使用的 block 數量,共用體中只有 count 字段有意義,_padding 是為了讓 ref 字段占 8 個字節,這個特性在 usedpools 初始化的時候有用
    block *freeblock; // pool 中可用來進行分配的 block 單鏈表的頭指針
    struct pool_header *nextpool; // 在 arena_object.freepools 或 usedpools 中指向下一個 pool
    struct pool_header *prevpool; // 在 usedpools 中指向上一個 pool
    uint arenaindex; // pool 所在 arena 在 arenas 數組中的索引
    uint szidx; // pool 中 block 大小的分級
    uint nextoffset; // 需要新的 block 可以從 nextoffset 處分配
    uint maxnextoffset; // nextoffset 最大有效值
};

typedef struct pool_header *poolp;

每個 pool 一旦被分配之后一定會處于 full、empty 和 used 這 3 種狀態中的一種。

  • full 所有的 block 都分配了

  • empty 所有的 block 都是空閑的,都可用于分配,所有處于 empty 狀態的 pool 都在其所在 arena_object 的 freepools 字段表示的單鏈表中

  • used 有已分配的 block,也有空閑的 block,所有處于 used 狀態的 pool 都在全局數組 usedpools 中某個元素指向的雙向循環鏈表中

usedpools 是內存分配最常訪問的數據結構,分配內存時先計算申請的內存大小對應的 block 分級 i,usedpools[i+i] 指向的就是屬于分級 i 的所有處于 used 狀態的 pool 構成的雙向循環鏈表的頭結點,如果鏈表不空就從頭結點中選擇一個空閑 block 分配。接下來分析一下為什么 usedpools[i+i] 指向的就是屬于分級 i 的 pool 所在的鏈表。

usedpools 的原始定義如下

#define PTA(x)  ((poolp )((uint8_t *)&(usedpools[2*(x)]) - 2*sizeof(block *)))
#define PT(x)   PTA(x), PTA(x)
static poolp usedpools[2 * ((NB_SMALL_SIZE_CLASSES + 7) / 8) * 8] = { 
    PT(0), PT(1), PT(2), PT(3), PT(4), PT(5), PT(6), PT(7),
    …
}

將宏定義稍微展開一下

static poolp usedpools[64] = { 
    PTA(0), PTA(0), PTA(1), PTA(1), PTA(2), PTA(2), PTA(3), PTA(3),
    PTA(4), PTA(4), PTA(5), PTA(5), PTA(6), PTA(6), PTA(7), PTA(7),
    …
}

PTA(x) 表示數組 usedpools 中第 2*x 個元素的地址減去兩個指針的大小也就是 16 字節(64 位機器),假設數組 usedpools 首地址為 1000,則數組初始化的值如下圖所示

Python內存管理器怎么實現池化技術

假設 i = 2,則 usedpools[i+i] = usedpools[4] = 1016,數組元素的類型為 poolp 也就是 struct pool_header *,如果認為 1016 存儲的是 struct pool_header,那么 usedpools[4] 和 usedpools[5] 的值也就是地址 1032 和 1040 存儲的值,分別是字段 nextpool 和 prevpool 的值,可以得到

usedpools[4]->prevpool = usedpools[4]->nextpool = usedpools[4] = 1016

usedpools[4] 用指針 p 表示就有 p->prevpool = p->nextpool = p,那么 p 就是雙向循環鏈表的哨兵節點,初始化的時候哨兵節點的前后指針都指向自己,表示當前鏈表為空。

雖然 usedpools 的定義非常繞,但是這樣定義有個好處就是省去了哨兵節點的數據域,只保留前后指針,可以說是將節省內存做到了極致。

下面分別看看源碼是怎么實現內存分配和釋放的邏輯的,下文中的源碼基于 Python 3.10.4。另外說明一下,源碼中有比本文詳細的多注釋說明,有興趣的讀者可以直接看源碼,本文為了代碼不至于過長會對代碼做簡化處理并且省略掉了大部分注釋。

內存分配

內存分配的主邏輯在函數 pymalloc_alloc 中,簡化后代碼如下

static inline void*
pymalloc_alloc(void *ctx, size_t nbytes)
{  
    // 計算請求的內存大小 ntybes 所對應的內存分級 size
    uint size = (uint)(nbytes - 1) >> ALIGNMENT_SHIFT;
    // 找到屬于內存分級 size 的 pool 所在的雙向循環鏈表的頭指針 pool
    poolp pool = usedpools[size + size];
    block *bp;
    // pool != pool->nextpool,說明 pool 不是哨兵節點,是真正的 pool
    if (LIKELY(pool != pool->nextpool)) {
        ++pool->ref.count;
        // 將 pool->freeblock 指向的 block 分配給 bp,因為 pool 是從 usedpools 中取的,
        // 根據 usedpools 的定義,pool->freeblock 指向的一定是空閑的 block
        bp = pool->freeblock;
        // 如果將 bp 分配之后 pool->freeblock 為空,需要從 pool 中劃分一個空閑 block
        // 到 pool->freeblock 鏈表中留下次分配使用
        if (UNLIKELY((pool->freeblock = *(block **)bp) == NULL)) {
            pymalloc_pool_extend(pool, size);
        }
    }
    // 如果沒有對應內存分級的可用 pool,就從 arena 中分配一個 pool 之后再從中分配 block
    else {
        bp = allocate_from_new_pool(size);
    }
    
    return (void *)bp;
}

主體邏輯還是比較清晰的,代碼中注釋都做了說明,不過還是要解釋一下下面的這個判斷語句。

if (UNLIKELY((pool->freeblock = *(block **)bp) == NULL))

前文已經介紹過 pool->freeblock 表示 pool 中可用來進行分配的 block 所在單鏈表的頭指針,類型為 block*,但是 block 的定義為 typedef uint8_t block;,并不是一個結構體,所以沒有指針域,那么是怎么實現單鏈表的呢。考慮到 pool->freeblock 的實際含義,只需要把空閑 block 用單鏈表串起來就可以了,不需要數據域,Python 內存管理器把空閑 block 內存的起始 8 字節(64 位機器)當做虛擬的 next 指針,指向下一個空閑 block,具體是通過 *(block **)bp 實現的。首先用 (block **) 將 bp 轉換成 block 的二級指針,然后用 * 解引用,將 bp 指向內存的首地址內容轉換成 (block *) 類型,表示下一個 block 的地址,不得不說,C 語言真的是可以為所欲為。再來看一下上面判斷語句,首先將 bp 的下一個空閑 block 地址賦值給 pool->freeblock,如果是 NULL 證明沒有更多空閑 block,需要調用 pymalloc_pool_extend 擴充。

pymalloc_pool_extend 的源碼簡化后如下

static void
pymalloc_pool_extend(poolp pool, uint size)
{
    // 如果 pool 還有更多空間,就劃分一個空閑 block 放到 pool->freeblock 中
    if (UNLIKELY(pool->nextoffset <= pool->maxnextoffset)) {
        pool->freeblock = (block*)pool + pool->nextoffset;
        pool->nextoffset += INDEX2SIZE(size);
        // pool->freeblock 只有一個 block,需要將虛擬的 next 指針置為 NULL
        *(block **)(pool->freeblock) = NULL;
        return;
    }

    // 如果沒有更多空間,需要將 pool 從 usedpools[size+size] 中移除
    poolp next;
    next = pool->nextpool;
    pool = pool->prevpool;
    next->prevpool = pool;
    pool->nextpool = next;

}

過程也很清晰,如果有更多空間就劃分一個 block 到 pool->freeblock,如果沒有更多空間就將 pool 從 usedpools[size+size] 中移除。pool->nextoffset 指向的是 pool 中從未被使用過內存的地址,分配 block 時候優先使用 pool->nextoffset 之前的空閑 block,這些空閑的 block 一般是之前分配過后來又被釋放到 pool->freeblock 中的。這種復用空閑 block 的方式讓 pool 更加經久耐用,如果每次都從 pool->nextoffset 劃分一個新的 block,pool 很快就會被消耗完,變成 full 狀態。

在 pymalloc_alloc 中如果沒有可用 pool 就會調用 allocate_from_new_pool 先分配一個新的 pool,再從新的 pool 中分配 block,其源碼簡化后如下

static void*
allocate_from_new_pool(uint size)
{
    // 沒有可用的 arena 就新申請一個
    if (UNLIKELY(usable_arenas == NULL)) {
        usable_arenas = new_arena();
        if (usable_arenas == NULL) {
            return NULL;
        }
        // 將新的 arena 作為 usable_arenas 鏈表的頭結點
        usable_arenas->nextarena = usable_arenas->prevarena = NULL;
        nfp2lasta[usable_arenas->nfreepools] = usable_arenas;
    }

    // 如果有可用 arena 就從中分配一個空閑 pool,并調整當前 arena 在 usable_arenas 中的位置,使 usable_arenas 按空閑 pool 的數量從小到大排序
    if (nfp2lasta[usable_arenas->nfreepools] == usable_arenas) {
        nfp2lasta[usable_arenas->nfreepools] = NULL;
    }
    if (usable_arenas->nfreepools > 1) {
        nfp2lasta[usable_arenas->nfreepools - 1] = usable_arenas;
    }

    // 執行到這里,usable_arenas->freepools 就是當前需要的可用 pool
    poolp pool = usable_arenas->freepools;
    // 更新 freepools 鏈表和 nfreepools 計數
    if (LIKELY(pool != NULL)) {
        usable_arenas->freepools = pool->nextpool;
        usable_arenas->nfreepools--;
        // 分配之后,如果 arena 中沒有空閑 pool,需要更新 usable_arenas 鏈表
        if (UNLIKELY(usable_arenas->nfreepools == 0)) {
            usable_arenas = usable_arenas->nextarena;
            if (usable_arenas != NULL) {
                usable_arenas->prevarena = NULL;
            }
        }
    }
    // 如果當前 arena 中沒有可用 pool,就重新劃分一個
    else {
        pool = (poolp)usable_arenas->pool_address;
        pool->arenaindex = (uint)(usable_arenas - arenas);
        pool->szidx = DUMMY_SIZE_IDX;
        usable_arenas->pool_address += POOL_SIZE;
        --usable_arenas->nfreepools;
        // 劃分之后,如果 arena 中沒有空閑 pool,需要更新 usable_arenas 鏈表
        if (usable_arenas->nfreepools == 0) {
            usable_arenas = usable_arenas->nextarena;
            if (usable_arenas != NULL) {
                usable_arenas->prevarena = NULL;
            }
        }
    }

    // 執行到這里,變量 pool 就是找到的可用 pool,將其置為鏈表 usedpools[size+size] 的頭節點
    block *bp;
    poolp next = usedpools[size + size];
    pool->nextpool = next;
    pool->prevpool = next;
    next->nextpool = pool;
    next->prevpool = pool;
    pool->ref.count = 1;
    // 如果 pool 的內存分級跟請求的一致,直接從中分配一個 block 返回
    // 證明這個 pool 之前被使用之后又釋放到 freepools 中了
    // 并且當時使用的時候內存分級也是 size
    if (pool->szidx == size) {
        bp = pool->freeblock;
        pool->freeblock = *(block **)bp;
        return bp;
    }
    
    // 執行到這里,說明 pool 是 arena 新劃分的,需要對其進行初始化
    // 然后分配 block 返回
    pool->szidx = size;
    size = INDEX2SIZE(size);
    bp = (block *)pool + POOL_OVERHEAD;
    pool->nextoffset = POOL_OVERHEAD + (size << 1);
    pool->maxnextoffset = POOL_SIZE - size;
    pool->freeblock = bp + size;
    *(block **)(pool->freeblock) = NULL;
    return bp;
}

這段代碼比較長,歸納一下做了下面 3 件事

  • 如果沒有可用的 arena 就重新申請一個

  • 從可用的 arena 中分配一個新的 pool

  • 從分配的 pool 中分配空閑的 block

首先是 arena 的申請,申請流程在函數 new_arena() 中,申請完之后將對應的 arena_object 置為 雙線鏈表 usable_arenas 的頭結點,并且前后指針都置為 NULL,因為只有在沒有可用 arena 的時候才回去調用 new_arena(),所以申請之后系統里只有一個可用 arena。另外還有一個操作如下

nfp2lasta[usable_arenas->nfreepools] = usable_arenas;

nfp2lasta 是一個數組,nfp2lasta[i] 表示的是在 usable_arenas 鏈表中,空閑 pool 的數量為 i 的所有 arena 中最后一個 arena。前文已經說明 usable_arenas 是按照 arena 中空閑 pool 的數量從小到大排序的,為了維護 usable_arenas 的有序性,在插入或刪除一個 arena 的時候需要找到對應的位置,時間復雜度為 O(N),為了避免線性搜索,Python 3.8 引入了 nfp2lasta,將時間復雜度降為常量級別。

有了可用的 arena 就可以從中分配 pool 了,分配 pool 之后 arena->nfreepools 就會減少,需要更新 nfp2lasta,由于使用的是鏈表 usable_arenas 的頭結點,并且是減少其空閑 pool 數量,所以整個鏈表依然有序。接下來優先復用 arena->freepools 中空閑的 pool,如果沒有就從 arena->pool_address 指向的未使用內存處新劃分一個 pool,這點跟 pool 中復用空閑 block 的策略是一樣的。

分配了可用的 pool,先將其置為鏈表 usedpools[size+size] 的頭結點,然后從中分配 block,如果 pool 不是從新分配的 arena 獲得的,那么 pool 就是之前初始化使用之后釋放掉的,如果 pool 的分級恰好就是請求的內存分級那么直接從 pool->freeblock 分配 block,否則需要將 pool 重新初始化,當然如果 pool 來自新分配的 arena 也要進行初始化。初始化的時候,先將第一個 block 的地址進行內存對齊,然后將 pool->freeblock 指向第 2 個 block 留下次分配使用(第 1 個 block 本次要返回),將 pool->nextoffset 指向第 3 個 block,在下次劃分新的 block 時使用。

內存釋放

內存釋放的主邏輯在 pymalloc_free 函數中,代碼簡化后如下

static inline int
pymalloc_free(void *ctx, void *p)
{
    // 假設 p 是 pool 分配的,計算 p 所在 pool 的首地址
    poolp pool = POOL_ADDR(p);
    // 如果 p 不是內存管理器分配的直接返回
    if (UNLIKELY(!address_in_range(p, pool))) {
        return 0;
    }
    
    // 將 p 指向的 block 歸還給 pool,置為 pool->freeblock 的頭結點
    block *lastfree = pool->freeblock;
    *(block **)p = lastfree;
    pool->freeblock = (block *)p;
    pool->ref.count--;
    // 如果 pool 原來處于 full 狀態,現在有一個空閑的 block 就變成了 used 狀態
    // 需要將其作為頭結點插到 usedpools[size+size] 中
    if (UNLIKELY(lastfree == NULL)) {
        insert_to_usedpool(pool);
        return 1;
    }

    if (LIKELY(pool->ref.count != 0)) {
        return 1;
    }

    // 如果 block 釋放之后,其所在 pool 所有的 block 都是空閑狀態,
    // 將 pool 從 usedpools[size+size] 中移到 arena->freepools 
    insert_to_freepool(pool);
    return 1;
}

pymalloc_free 函數的邏輯也很清晰

  • 計算地址 p 所在 pool 首地址,前文介紹過每個 pool 首地址都是 POOL_SIZE 的整數倍,所以將 p 的低位置 0 就得到了 pool 的地址

  • address_in_range(p, pool) 判斷 p 是否是由 pool 分配的,如果不是直接返回

  • 將 p 指向的 block 釋放掉,被 pool->freeblock 回收

  • 如果 pool 開始為 full 狀態,那么回收 block 之后就是 used 狀態,調用函數 insert_to_usedpool(pool) 將其置為 usedpools[size+size] 的頭結點。這里的策略跟 usable_arenas 一樣,優先使用快滿的 pool,讓比較空閑的 pool 有較高的概率被釋放掉。

  • 如果 pool 回收 block 之后變成 empty 狀態,需要調用 insert_to_freepool(pool) 將 pool 也釋放掉

address_in_range 函數如下

address_in_range(void *p, poolp pool)
{
    uint arenaindex = *((volatile uint *)&pool->arenaindex);
    return arenaindex < maxarenas &&
        (uintptr_t)p - arenas[arenaindex].address < ARENA_SIZE &&
        arenas[arenaindex].address != 0;
}

這段邏輯能在常量時間內判斷出 p 是否由 pool 分配,但是存在一個可能出問題的地方,畢竟這里的 pool 是在假設 p 是由 pool 分配的前提下計算出來的,有可能 pool 指向的地址可能還沒被初始化,pool->arenaindex 操作可能會出錯。Python 3.10 在這個 commit 中利用基數樹來判斷任意一個地址 p 是不是由內存管理器分配的,避免了可能出現的內存訪問錯誤。

insert_to_usedpool 函數中只是簡單的指針操作就不展開了,insert_to_freepool 稍微復雜一點,下面再展開一下

static void
insert_to_freepool(poolp pool)
{
    poolp next = pool->nextpool;
    poolp prev = pool->prevpool;
    next->prevpool = prev;
    prev->nextpool = next;
    // 將 pool 置為 ao->freepools 頭結點
    struct arena_object *ao = &arenas[pool->arenaindex];
    pool->nextpool = ao->freepools;
    ao->freepools = pool;
    uint nf = ao->nfreepools;
    struct arena_object* lastnf = nfp2lasta[nf];
    // 如果 arena 是排在最后的包含 nf 個空閑 pool 的 arena,
    // 需要將 nfp2lasta[nf] 置為 arena 的前驅結點或 NULL
    if (lastnf == ao) { /* it is the rightmost */
        struct arena_object* p = ao->prevarena;
        nfp2lasta[nf] = (p != NULL && p->nfreepools == nf) ? p : NULL;
    }
    ao->nfreepools = ++nf;

    // 如果 pool 釋放后 arena 變成完全空閑狀態,并且系統中還有其他可用 arena,
    // 需要將 arena 從 usable_arenas 中移除并調用 free() 函數將其釋放歸還給操作系統
    if (nf == ao->ntotalpools && ao->nextarena != NULL) {
        if (ao->prevarena == NULL) {
            usable_arenas = ao->nextarena;
        }
        else {
            ao->prevarena->nextarena = ao->nextarena;
        }
        if (ao->nextarena != NULL) {
            ao->nextarena->prevarena = ao->prevarena;
        }
        ao->nextarena = unused_arena_objects;
        unused_arena_objects = ao;
        arena_map_mark_used(ao->address, 0);
        _PyObject_Arena.free(_PyObject_Arena.ctx, (void *)ao->address, ARENA_SIZE);
        ao->address = 0;          
        --narenas_currently_allocated;
        return;
    }
    // 如果 pool 釋放后 arena 從滿變成可用,需要將其置為 usable_arenas 頭結點,
    // 因為 arena 空閑 pool 數量為 1,作為頭結點不會破壞 usable_arenas 有序性
    if (nf == 1) {
        ao->nextarena = usable_arenas;
        ao->prevarena = NULL;
        if (usable_arenas)
            usable_arenas->prevarena = ao;
        usable_arenas = ao;
        if (nfp2lasta[1] == NULL) {
            nfp2lasta[1] = ao;
        }
        return;
    }

    if (nfp2lasta[nf] == NULL) {
        nfp2lasta[nf] = ao;
    } 
    // 如果 arena 本來就是包含 lastnf 個空閑 pool 的最后一個,現在空閑 pool 數量加 1,
    // 整個 usable_arenas 還是有序的
    if (ao == lastnf) {
        return;
    }

    // arena->nfreepools 的增加導致 usable_arenas 失序,
    // 重新調整 arena 在 usable_arenas 的位置
    if (ao->prevarena != NULL) {
        ao->prevarena->nextarena = ao->nextarena;
    }
    else {
        usable_arenas = ao->nextarena;
    }
    ao->nextarena->prevarena = ao->prevarena;
    ao->prevarena = lastnf;
    ao->nextarena = lastnf->nextarena;
    if (ao->nextarena != NULL) {
        ao->nextarena->prevarena = ao;
    }
    lastnf->nextarena = ao;
}

首先將這個空閑的 pool 置為 ao->freepools 的頭結點,這樣可以保證最后釋放的 pool 會最先被使用,提高訪存效率,因為之前釋放的 pool 可能被置換出了物理內存。然后根據不同情況更新 nfp2lasta,便于后續維護 usable_arenas 的有序性。接著根據 pool 釋放前后其所在 arena 狀態的變化做不同操作。

  • 如果 arena 由可用狀態變成空閑狀態,并且系統中還有其他可用 arena,就調用 free() 將 arena 釋放掉歸還給操作系統。如果系統中僅有這一個空閑 arena 就不釋放,避免內存抖動。

  • 如果 arena 由不可用狀態(所有 pool 都分配了)變成可用狀態,將其置為 usable_arenas 的頭結點。

  • 如果 pool 釋放前后 arena 都是可用狀態,也就是一直都在 usable_arenas 鏈表中,如果其可用 pool 數量的增加導致 usable_arenas 鏈表失序,需要移動 arena 到合適位置來保持 usable_arenas 的有序性。

感謝各位的閱讀,以上就是“Python內存管理器怎么實現池化技術”的內容了,經過本文的學習后,相信大家對Python內存管理器怎么實現池化技術這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

韶关市| 灌南县| 鱼台县| 榆林市| 浦江县| 肃宁县| 紫云| 称多县| 白城市| 杨浦区| 澄江县| 肇东市| 衡南县| 荆门市| 晋宁县| 南充市| 梅州市| 水富县| 弥勒县| 香格里拉县| 八宿县| 隆子县| 邵阳县| 西丰县| 安庆市| 额尔古纳市| 多伦县| 澳门| 湾仔区| 石景山区| 泾阳县| 五台县| 都兰县| 丹江口市| 石楼县| 民和| 织金县| 东至县| 咸阳市| 黔东| 大石桥市|