1. 程式人生 > >Python的記憶體管理機制(一 小記憶體塊的申請)

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