Python的記憶體管理機制(一 小記憶體塊的申請)
哈哈哈,看的有點兒興奮了,筆記下:
在Python的內部同時維護著巨集和記憶體管理函式兩種記憶體管理機制,巨集定義可以節省一次函式呼叫的開銷,
提高執行效率,但同時,使用巨集是危險的,因為隨著Python的演進,記憶體管理機制的實現可能會發生改變,因為巨集的名字雖然是不會改變的,但其實現程式碼可能會發生改變,導致舊的巨集編寫的C擴充套件與新版本的Python產生二進位制不相容,這樣十分危險,所以,在Python的內部維護了函式和巨集兩套介面,自己編寫擴充套件時,最好使用函式
1 小塊的記憶體池
在Python執行的過程中,很多時候都是小塊記憶體的申請,申請後又很快釋放,也就意味著大量的malloc和free
操作,導致作業系統頻繁的在使用者態和核心態之間進行切換,影響py的執行效率,為了解決這個問題Py引入了快取池的機制(Pymalloc),整個小塊記憶體的記憶體池可以視為一個層次結構,在這個層次可以分為4層,從上到下依次為:block , pool , arean和記憶體池
1.1 先看下block吧:
在最底層block是一個確定大小的記憶體,在py中有很多種block,每個block有不同的記憶體大小,這個記憶體的大小稱為size class,可以不記得,記得這是最小單元就好,所有block事8位元組對齊的,同時,py為block的大小設定了一個上限,當申請的記憶體大小小於這個上限時,py可以使用不同種類的block來滿足需求,大於這個上限的時候,py轉交給第一次的記憶體管理機制來處理,即pyMem函式族,在py2.5版本,這個值為256k
不同種類的block的大小分別為 8,16,32 ..... ,256,每個size class對應一個size class index ,這個index從0開始,假設申請記憶體為28,會得到一個32的block,能滿足的最小值
1.2 那麼超過256的記憶體申請怎麼辦呢 ? 來一起看下pool吧:
/* 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 */
uint nextoffset;/* bytes to virgin block */
uint maxnextoffset;/* largest valid nextoffset */
};
一組block的集合稱為pool,也就是一個pool管理著多個有固定大小的block,一個pool的大小通常是一個記憶體頁,py將其定義為4k,一個pool裡邊的block的大小是固定的,比如,可能管理了100個32k的block,也可能管理了100個64k的block,但是不可能一個pool裡既有32k的block也有64k的block,每一個pool都和一個size聯絡到一起,更確切的說是和一個size class index聯絡到一起,這就是pool header裡邊的sindex的意義所在
一起看下pool是怎麼把block組合到一起的:
/*
* Initialize the pool header, set up the free list to
* contain just the second block, and return the first
* 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;
UNLOCK();
return (void *)bp;
pool中的第一塊記憶體是分配給pool_header的,所以pool的結構體中ref.count是1,第一塊地址被分配後返回bp的是一個實際地址(也就是申請pool之後的返回),在這個地址之後會有進4k的記憶體都是實際可用的,但是可以肯定的是申請記憶體的函式只會使用[bp,bp+size]這個區間的記憶體,這是size class index保證的,
我該怎麼畫圖呢 ? ......先跳過了
/*
* There is a used pool for this size class.
* Pick up the head block of its free list.
*/
++pool->ref.count;
bp = pool->freeblock;
assert(bp != NULL);
if ((pool->freeblock = *(block **)bp) != NULL) {
UNLOCK();
return (void *)bp;
}
/*
* Reached the end of the free list, try to extend it.
*/
if (pool->nextoffset <= pool->maxnextoffset) {
/* There is room for another block. */
pool->freeblock = (block*)pool +
pool->nextoffset;
pool->nextoffset += INDEX2SIZE(size);
*(block **)(pool->freeblock) = NULL;
UNLOCK();
return (void *)bp;
}
假設連續申請5塊大小為28位元組的記憶體,由於28位元組的對應size class index為3,所以會申請5塊32位元組的記憶體,原來freeblock執行的是下一個可用bock的起始地址,再次申請是,只需要返回freeblock指向的地址就可
以了,很顯然,freeblock需要前進,指向下一個可用block,此時nextoffset就發光發熱了,在pool header裡,nextoffset和maxoffset是對pool中的block步進迭代的變數,偏移地址恰好是下一個block的可用地址,在分配
完一個block之後,freeblock和nextoffset就移動一個block的距離,maxoffset則定義了最大的可用大小,劃定了block的邊界,nextoffset > maxoffset的時候,pool中的block就遍歷完了,恩,就是這樣的,申請->前進->
申請->前進......
但是假如一個block用完釋放了,假如申請了連續5塊32位元組的記憶體,2用完釋放了,下一個申請是用2還是6 ?
一旦py執行會有大量的這種記憶體碎片產生,py必須用一種機制把離散的記憶體管理起來,這就是自由的block連結串列,連結串列的關鍵就是header中的freeblock,回到上邊,pool申請初始化完成之後,會指向一個有效地址,為下一個可分配的記憶體地址,還設定了一個*freeblock,等到有記憶體釋放的時候,freeblock就會變成一個小精靈,
在這4k的記憶體上靈活舞動,哈哈~~
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 */
*(block **)p = lastfree = pool->freeblock;
pool->freeblock = (block *)p;
if (lastfree) {
struct arena_object* ao;
uint nf; /* ao->nfreepools */
/* freeblock wasn't NULL, so the pool wasn't full,
* and the pool is in a usedpools[] list.
*/
if (--pool->ref.count != 0) {
/* pool isn't empty: leave it in usedpools */
UNLOCK();
return;
}
來看下,block釋放的時候,freeblock指向一個有效的pool地址,但是*freeblock是NULL,假設釋放的block是blockA,在pool->freeblock = (block *)p;的時候,freeblock的值被更新了,指向了blockA的首地址,在釋放下一個block的時候,同樣通過
*(block **)p = lastfree = pool->freeblock;
pool->freeblock = (block *)p;
把釋放的block連線到一起,形成自由連結串列,在需要記憶體的時候可以很方便的遍歷這個連結串列,優先使用被釋放的記憶體,當pool->freeblock為NULL的時候就不存在離散的自由連結串列了,分配nextblock即可,nextoffset > maxoffset時,一個pool就用完了,怎麼辦 ?哈哈,莫著急,一定是存在一個pool的集合的,這就是下邊要說的arean