基本閱讀完了, 只是沒時間梳理, 趁著這今天時間比較空
逐步梳理, 發上來……也算是小結下, 要開始準備簡歷找工作了>_
這篇略長, 帶很多圖, 所以一分為二
Python的記憶體管理架構
基本分層
在Objects/obmalloc.c
原始碼中, 給了一個分層劃分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
_____ ______ ______ ________ [ int ] [ dict ] [ list ] ... [ string ] Python core | +3 | <----- Object-specific memory -----> | <-- Non-object memory --> | _______________________________ | | [ Python's object allocator ] | | +2 | ####### Object memory ####### | <------ Internal buffers ------> | ______________________________________________________________ | [ Python's raw memory allocator (PyMem_ API) ] | +1 | <----- Python memory (under PyMem manager's control) ------> | | __________________________________________________________________ [ Underlying general-purpose allocator (ex: C library malloc) ] 0 | <------ Virtual memory allocated for the python process -------> | ========================================================================= _______________________________________________________________________ [ OS-specific Virtual Memory Manager (VMM) ] -1 | <--- Kernel dynamic storage allocation & management (page-based) ---> | __________________________________ __________________________________ [ ] [ ] -2 | <-- Physical memory: ROM/RAM --> | | <-- Secondary storage (swap) --> | |
可以看到
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
layer 3: Object-specific memory(int/dict/list/string....) Python 實現並維護 更高抽象層次的記憶體管理策略, 主要是各類特定物件的緩衝池機制. 具體見前面幾篇涉及的記憶體分配機制 layer 2: Python's object allocator Python 實現並維護 實現了建立/銷燬Python物件的介面(PyObject_New/Del), 涉及物件引數/引用計數等 layer 1: Python's raw memory allocator (PyMem_ API) Python 實現並維護, 包裝了第0層的記憶體管理介面, 提供統一的raw memory管理介面 封裝的原因: 不同作業系統 C 行為不一定一致, 保證可移植性, 相同語義相同行為 layer 0: Underlying general-purpose allocator (ex: C library malloc) 作業系統提供的記憶體管理介面, 由作業系統實現並管理, Python不能干涉這一層的行為 |
第三層layer 3
前面已經介紹過了, 幾乎每種常用的資料型別都伴有一套緩衝池機制.
在這裡, 我們關注的是layer 2/1
簡要介紹下layer 1
, 然後重點關注layer 2
, 這才是重點
layer 1: PyMem_ API
PyMem_ API
是對作業系統記憶體管理介面進行的封裝
檢視pymem.h
可以看到
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
// Raw memory interface // 這裡存在三個巨集定義, 巨集可以避免一次函式呼叫的開銷, 提高執行效率 // 不允許非配空間大小為0的記憶體空間 #define PyMem_MALLOC(n) ((size_t)(n) > (size_t)PY_SSIZE_T_MAX ? NULL : malloc((n) ? (n) : 1)) #define PyMem_REALLOC(p, n) ((size_t)(n) > (size_t)PY_SSIZE_T_MAX ? NULL : realloc((p), (n) ? (n) : 1)) #define PyMem_FREE free // 這裡做了三個函式的宣告, 平臺獨立的 malloc/realloc/free PyAPI_FUNC(void *) PyMem_Malloc(size_t); PyAPI_FUNC(void *) PyMem_Realloc(void *, size_t); PyAPI_FUNC(void) PyMem_Free(void *); // ============================================================ // Type-oriented memory interface // 這裡還有三個型別相關的記憶體介面, 批量分配/重分配 n 個 型別為 type記憶體 #define PyMem_New(type, n) ( ((size_t)(n) > PY_SSIZE_T_MAX / sizeof(type)) ? NULL : ( (type *) PyMem_Malloc((n) * sizeof(type)) ) ) #define PyMem_NEW(type, n) ( ((size_t)(n) > PY_SSIZE_T_MAX / sizeof(type)) ? NULL : ( (type *) PyMem_MALLOC((n) * sizeof(type)) ) ) #define PyMem_Resize(p, type, n) ( (p) = ((size_t)(n) > PY_SSIZE_T_MAX / sizeof(type)) ? NULL : (type *) PyMem_Realloc((p), (n) * sizeof(type)) ) #define PyMem_RESIZE(p, type, n) ( (p) = ((size_t)(n) > PY_SSIZE_T_MAX / sizeof(type)) ? NULL : (type *) PyMem_REALLOC((p), (n) * sizeof(type)) ) |
然後object.c
中, 我們關注實現
, 三個實現
的函式呼叫了對應的巨集
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// 使用 C 寫Python擴充套件模組時使用函式而不是對應的巨集 void * PyMem_Malloc(size_t nbytes) { return PyMem_MALLOC(nbytes); } void * PyMem_Realloc(void *p, size_t nbytes) { return PyMem_REALLOC(p, nbytes); } void PyMem_Free(void *p) { PyMem_FREE(p); } |
這些介面都相對簡單
好了, 結束, 開始關注layer 2: Python's object allocator
Python 的記憶體分配策略
先來看Objects/obmalloc.c
中的一段註釋
1 2 3 4 5 6 7 8 9 |
/* * "Memory management is where the rubber meets the road -- if we do the wrong * thing at any level, the results will not be good. And if we don't make the * levels work well together, we are in serious trouble." (1) * * (1) Paul R. Wilson, Mark S. Johnstone, Michael Neely, and David Boles, * "Dynamic Storage Allocation: A Survey and Critical Review", * in Proc. 1995 Int'l. Workshop on Memory Management, September 1995. */ |
Python引入了記憶體池機制, 用於管理對小塊記憶體的申請和釋放
邏輯
1 2 |
1. 如果要分配的記憶體空間大於 SMALL_REQUEST_THRESHOLD bytes(512 bytes), 將直接使用layer 1的記憶體分配介面進行分配 2. 否則, 使用不同的block來滿足分配需求 |
整個小塊記憶體池可以視為一個層次結構
1 2 3 4 |
1. 記憶體池(概念上的, 標識Python對於整個小塊記憶體分配和釋放的記憶體管理機制) 2. arena 3. pool 4. block |
block
Python記憶體的最小單位, 所有block長度都是8位元組對齊的
注意這裡block只是一個概念, 在原始碼中並沒有實體存在.
不同型別block, 對應不同記憶體大小, 這個記憶體大小的值被稱為size class
.
不同長度的block
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
* Request in bytes Size of allocated block Size class idx * ---------------------------------------------------------------- * 1-8 8 0 * 9-16 16 1 * 17-24 24 2 * 25-32 32 3 * 33-40 40 4 * 41-48 48 5 * 49-56 56 6 * 57-64 64 7 * 65-72 72 8 * ... ... ... * 497-504 504 62 * 505-512 512 63 * * 0, SMALL_REQUEST_THRESHOLD + 1 and up: routed to the underlying * allocator. */ |
例如
1 |
申請一塊大小28位元組的記憶體, 實際從記憶體中劃到32位元組的一個block (從size class index為3的pool裡面劃出) |
圖示:
注意: 這裡有個Size class idx
, 這個主要為了後面pool中用到
size class
和size class index
之間的轉換
1 2 3 4 5 6 7 8 9 10 11 12 |
#define ALIGNMENT 8 /* must be 2^N */ #define ALIGNMENT_SHIFT 3 #define ALIGNMENT_MASK (ALIGNMENT - 1) // size class index => size class #define INDEX2SIZE(I) (((uint)(I) + 1) size class index size = (uint)(nbytes - 1) >> ALIGNMENT_SHIFT; /* 即 (8 - 1) / 8 = 0 (16 - 8) / 8 = 1 */ |
pool
pool管理block, 一個pool管理著一堆有固定大小的記憶體塊
本質: pool管理著一大塊記憶體, 它有一定的策略, 將這塊大的記憶體劃分為多個大小一致的小塊記憶體.
pool size
在Python中, 一個pool的大小通常為一個系統記憶體頁. 4kB
1 2 3 4 5 6 7 |
obmalloc.c #define SYSTEM_PAGE_SIZE (4 * 1024) #define SYSTEM_PAGE_SIZE_MASK (SYSTEM_PAGE_SIZE - 1) #define POOL_SIZE SYSTEM_PAGE_SIZE /* must be 2^N */ #define POOL_SIZE_MASK SYSTEM_PAGE_SIZE_MASK |
pool組成
pool的4kB記憶體 = pool_header + block集合(N多大小一樣的block)
pool_header
1 2 3 4 5 6 7 8 9 10 11 12 |
/* Pool for small blocks. */ struct pool_header { union { block *_padding; uint count; } ref; /* number of allocated blocks */ block *freeblock; /* pool's free list head */ struct pool_header *nextpool; /* next pool of this size class */ struct pool_header *prevpool; /* previous pool "" */ uint arenaindex; /* index into arenas of base adr */ uint szidx; /* block size class index */ - size class index uint nextoffset; /* bytes to virgin block */ uint maxnextoffset; /* largest valid nextoffset */ }; |
pool_header的作用
1 2 3 4 |
1. 與其他pool連結, 組成雙向連結串列 2. 維護pool中可用的block, 單連結串列 3. 儲存 szidx , 這個和該pool中block的大小有關係, (block size=8, szidx=0), (block size=16, szidx=1)...用於記憶體分配時匹配到擁有對應大小block的pool 4. arenaindex, 後面說 |
結構圖:
pool初始化
從記憶體中初始化一個全新的空的pool
Objects/obmalloc.c
的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
void * PyObject_Malloc(size_t nbytes) { ... init_pool: // 1. 連線到 used_pools 雙向連結串列, 作為表頭 // 注意, 這裡 usedpools[0] 儲存著 block size = 8 的所有used_pools的表頭 /* Frontlink to used pools. */ next = usedpools[size + size]; /* == prev */ pool->nextpool = next; pool->prevpool = next; next->nextpool = pool; next->prevpool = pool; pool->ref.count = 1; // 如果已經初始化過了...這裡看初始化, 跳過 if (pool->szidx == size) { /* Luckily, this pool last contained blocks * of the same size class, so its header * and free list are already initialized. */ bp = pool->freeblock; pool->freeblock = *(block **)bp; UNLOCK(); return (void *)bp; } /* * Initialize the pool header, set up the free list to * contain just the second block, and return the first * block. */ // 開始初始化pool_header // 這裡 size = (uint)(nbytes - 1) >> ALIGNMENT_SHIFT; 其實是Size class idx, 即szidx pool->szidx = size; // 計算獲得每個block的size size = INDEX2SIZE(size); // 注意 #define POOL_OVERHEAD ROUNDUP(sizeof(struct pool_header)) // bp => 初始化為pool + pool_header size, 跳過pool_header的記憶體 bp = (block *)pool + POOL_OVERHEAD; // 計算偏移量, 這裡的偏移量是絕對值 // #define POOL_SIZE SYSTEM_PAGE_SIZE /* must be 2^N */ // POOL_SIZE = 4kb, POOL_OVERHEAD = pool_header size // 下一個偏移位置: pool_header size + 2 * size pool->nextoffset = POOL_OVERHEAD + (size maxnextoffset = POOL_SIZE - size; // freeblock指向 bp + size = pool_header size + size pool->freeblock = bp + size; // 賦值NULL *(block **)(pool->freeblock) = NULL; UNLOCK(); return (void *)bp; } |
初始化後的圖
pool進行block分配 – 0 總體程式碼
總體分配的程式碼如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
if (pool != pool->nextpool) { // /* * There is a used pool for this size class. * Pick up the head block of its free list. */ ++pool->ref.count; bp = pool->freeblock; // 指標指向空閒block起始位置 assert(bp != NULL); // 程式碼-1 // 調整 pool->freeblock (假設A節點)指向連結串列下一個, 即bp首位元組指向的下一個節點(假設B節點) , 如果此時!= NULL // 表示 A節點可用, 直接返回 if ((pool->freeblock = *(block **)bp) != NULL) { UNLOCK(); return (void *)bp; } // 程式碼-2 /* * Reached the end of the free list, try to extend it. */ // 有足夠的空間, 分配一個, pool->freeblock 指向後移 if (pool->nextoffset maxnextoffset) { /* There is room for another block. */ // 變更位置資訊 pool->freeblock = (block*)pool + pool->nextoffset; pool->nextoffset += INDEX2SIZE(size); *(block **)(pool->freeblock) = NULL; // 注意, 指向NULL UNLOCK(); // 返回bp return (void *)bp; } // 程式碼-3 /* Pool is full, unlink from used pools. */ // 滿了, 需要從下一個pool獲取 next = pool->nextpool; pool = pool->prevpool; next->prevpool = pool; pool->nextpool = next; UNLOCK(); return (void *)bp; } |
pool進行block分配 – 1 剛開始
記憶體塊尚未分配完, 且此時不存在回收的block, 全新進來的時候, 分配第一塊block
1 |
(pool->freeblock = *(block **)bp) == NULL |
所以進入的邏輯是程式碼-2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
bp = pool->freeblock; // 指標指向空閒block起始位置 ..... // 程式碼-2 /* * Reached the end of the free list, try to extend it. */ // 有足夠的空間, 分配一個, pool->freeblock 指向後移 if (pool->nextoffset maxnextoffset) { /* There is room for another block. */ // 變更位置資訊 pool->freeblock = (block*)pool + pool->nextoffset; pool->nextoffset += INDEX2SIZE(size); *(block **)(pool->freeblock) = NULL; // 注意, 指向NULL UNLOCK(); // 返回bp return (void *)bp; } |
結果圖示
pool進行block分配 – 2 回收了某幾個block
回收涉及的程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
void PyObject_Free(void *p) { poolp pool; block *lastfree; poolp next, prev; uint size; pool = POOL_ADDR(p); if (Py_ADDRESS_IN_RANGE(p, pool)) { /* We allocated this address. */ LOCK(); /* Link p to the start of the pool's freeblock list. Since * the pool had at least the p block outstanding, the pool * wasn't empty (so it's already in a usedpools[] list, or * was full and is in no list -- it's not in the freeblocks * list in any case). */ assert(pool->ref.count > 0); /* else it was empty */ // p被釋放, p的第一個位元組值被設定為當前freeblock的值 *(block **)p = lastfree = pool->freeblock; // freeblock被更新為指向p的首地址 pool->freeblock = (block *)p; // 相當於往list中頭插入了一個節點 ... } } |
沒釋放一個block, 該block就會變成 pool->freeblock
的頭節點, 而單連結串列一個節點如何指向下一個節點呢? 通過賦值, 節點記憶體空間儲存著下個節點的地址, 最後一個節點指向NULL
(知道上面程式碼-1
的判斷條件了吧>_
假設已經連續分配了5塊, 第1塊和第4塊被釋放
此時記憶體圖示
此時再一個block分配呼叫進來, 執行分配, 進入的邏輯是程式碼-1
1 2 3 4 5 6 7 8 |
bp = pool->freeblock; // 指標指向空閒block起始位置 // 程式碼-1 // 調整 pool->freeblock (假設A節點)指向連結串列下一個, 即bp首位元組指向的下一個節點(假設B節點) , 如果此時!= NULL // 表示 A節點可用, 直接返回 if ((pool->freeblock = *(block **)bp) != NULL) { UNLOCK(); return (void *)bp; } |
pool進行block分配 – 3 pool用完了
pool中記憶體空間都用完了, 進入程式碼-3
1 2 3 4 5 6 7 8 9 10 |
bp = pool->freeblock; // 指標指向空閒block起始位置 // 程式碼-3 /* Pool is full, unlink from used pools. */ // 滿了, 需要從下一個pool獲取 next = pool->nextpool; pool = pool->prevpool; next->prevpool = pool; pool->nextpool = next; UNLOCK(); return (void *)bp; |
獲取下一個pool(連結串列上每個pool的block size都是一致的)
好了, pool到此位置, 下篇進入arena
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
任選一種支付方式