在Python3.x中祝辣,Python內(nèi)部默認(rèn)的小塊內(nèi)存與大塊內(nèi)存的分界點(diǎn)是512字節(jié)脱吱,我們知道當(dāng)小于512字節(jié)的內(nèi)存請(qǐng)求帚呼,PyObject_Malloc會(huì)在內(nèi)存池中申請(qǐng)內(nèi)存撞羽,當(dāng)申請(qǐng)的內(nèi)存大于512字節(jié)阐斜,PyObject_Malloc的行為會(huì)蛻化為malloc的行為。例如當(dāng)我們申請(qǐng)一個(gè)28字節(jié)的內(nèi)存時(shí)诀紊,Python內(nèi)部會(huì)在內(nèi)存池尋找一塊能滿足需求的pool谒出,并從中取出一個(gè)block,而不會(huì)去需找arena邻奠,這實(shí)際上事由pool和arena自身的屬性確定的笤喳,pool有一個(gè)size概念的內(nèi)存管理抽象體,一個(gè)pool中的block總是有確定的類(lèi)型尺寸.pool_header結(jié)構(gòu)體定義中有一個(gè)szidx就是指定了對(duì)應(yīng)的pool分配出去的塊的最小的塊單位-類(lèi)型尺寸(size class)碌宴,然而arena沒(méi)有size idx的概念杀狡,這意味著同一個(gè)arena,在某個(gè)時(shí)刻贰镣,其托管的內(nèi)存池集合可能是size class為32字節(jié)的內(nèi)存池呜象,而另一個(gè)時(shí)刻該內(nèi)存池可能會(huì)被重新劃分膳凝,變?yōu)?4字節(jié)的block。
我們?cè)谟懻搯蝹€(gè)內(nèi)存池時(shí)恭陡,有涉及池狀態(tài)的概念蹬音。這里復(fù)習(xí)一下
- used:池中至少由一個(gè)block已經(jīng)正在使用,并且至少由一個(gè)block還未被使用子姜,這種狀態(tài)的內(nèi)存池由CPython的usedpool統(tǒng)一管理
- full狀態(tài):pool中所有block都已正在使用祟绊,這種狀態(tài)的pool在arena托管的池集合內(nèi),但不再arena的freepools鏈表中哥捕。
- empty狀態(tài):pool中的所有狀態(tài)都未被使用牧抽,處于這個(gè)狀態(tài)的pool的集合通過(guò)其pool_header結(jié)構(gòu)體的nextpool構(gòu)成一個(gè)鏈表,這個(gè)鏈表的表頭就是arena_object結(jié)構(gòu)體的freepools指針遥赚。
解讀usedpools數(shù)組
Python內(nèi)部通過(guò)使用usedpools數(shù)組扬舒,維護(hù)者所有處于used狀態(tài)的pool。當(dāng)申請(qǐng)內(nèi)存size class為N時(shí)凫佛,Python會(huì)通過(guò)usedpools查找到與N對(duì)應(yīng)的size idx可用的內(nèi)存池讲坎,從中分配一個(gè)類(lèi)型尺寸為N的塊,我們看看Objects/obmalloc.c源代碼的第1101行到1130行定義愧薛,其中的NB_SMALL_SIZE_CLASSES標(biāo)識(shí)當(dāng)前的CPython實(shí)現(xiàn)有多少個(gè)size class晨炕,對(duì)于CPython3.6之前表示有64種size class,CPython3.7之后有32種size class.
#define SMALL_REQUEST_THRESHOLD 512
#define NB_SMALL_SIZE_CLASSES (SMALL_REQUEST_THRESHOLD / ALIGNMENT)
參考如下源代碼的第1101行到1130行毫炉。
#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)
#if NB_SMALL_SIZE_CLASSES > 8
, PT(8), PT(9), PT(10), PT(11), PT(12), PT(13), PT(14), PT(15)
#if NB_SMALL_SIZE_CLASSES > 16
, PT(16), PT(17), PT(18), PT(19), PT(20), PT(21), PT(22), PT(23)
#if NB_SMALL_SIZE_CLASSES > 24
, PT(24), PT(25), PT(26), PT(27), PT(28), PT(29), PT(30), PT(31)
#if NB_SMALL_SIZE_CLASSES > 32
, PT(32), PT(33), PT(34), PT(35), PT(36), PT(37), PT(38), PT(39)
#if NB_SMALL_SIZE_CLASSES > 40
, PT(40), PT(41), PT(42), PT(43), PT(44), PT(45), PT(46), PT(47)
#if NB_SMALL_SIZE_CLASSES > 48
, PT(48), PT(49), PT(50), PT(51), PT(52), PT(53), PT(54), PT(55)
#if NB_SMALL_SIZE_CLASSES > 56
, PT(56), PT(57), PT(58), PT(59), PT(60), PT(61), PT(62), PT(63)
#if NB_SMALL_SIZE_CLASSES > 64
#error "NB_SMALL_SIZE_CLASSES should be less than 64"
#endif /* NB_SMALL_SIZE_CLASSES > 64 */
#endif /* NB_SMALL_SIZE_CLASSES > 56 */
#endif /* NB_SMALL_SIZE_CLASSES > 48 */
#endif /* NB_SMALL_SIZE_CLASSES > 40 */
#endif /* NB_SMALL_SIZE_CLASSES > 32 */
#endif /* NB_SMALL_SIZE_CLASSES > 24 */
#endif /* NB_SMALL_SIZE_CLASSES > 16 */
#endif /* NB_SMALL_SIZE_CLASSE
由于任意一個(gè)usedpools元素項(xiàng)的表達(dá)式為PT(x)等價(jià)于“PTA(x),PTA(x)”,那么usedpools的中間形式均等價(jià)如下
對(duì)于CPython 3.6之前的版本瓮栗,按8字節(jié)對(duì)齊,上面的usedpools數(shù)組形式,等價(jià)于以下代碼
static poolp usedpools[142] = {
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(70),PTA(70)
}
對(duì)于CPython3.7之后的版本瞄勾,按16字節(jié)對(duì)齊,上面的usedpools數(shù)組形式费奸,等價(jià)于以下代碼
static poolp usedpools[78] = {
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(38),PTA(38)
}
好了,從任意一個(gè)PTA(x)的元素項(xiàng),等價(jià)于
((poolp )((uint8_t *)&(usedpools[2*(x)]) - 2*sizeof(block *)))
其實(shí)整個(gè)usedpools數(shù)組的核心難點(diǎn)就是該P(yáng)TA(x)的宏等價(jià)表達(dá)式进陡。這個(gè)宏表達(dá)式意思是愿阐,指向usedpools[2x]的地址再向前偏移sizeof(pool_header.ref)+sizeof(block)后的地址,顯然我們知道在x86_64的C編譯器中剛好是16個(gè)字節(jié),從pool_header結(jié)構(gòu)體的定義發(fā)現(xiàn)從pool_header首個(gè)字節(jié)到nextpool字段首個(gè)字節(jié)的邊界趾疚,剛好相差16個(gè)字節(jié)的偏移量缨历。
我們?nèi)绾卫斫庹麄€(gè)usedpools的內(nèi)部運(yùn)行機(jī)制呢?相信看過(guò)《Python源碼剖析:深度探索動(dòng)態(tài)語(yǔ)言核心技術(shù)》這本的大伙們應(yīng)該知道那本書(shū)會(huì)給出直觀地給出下面的圖例盗蟆,包括國(guó)內(nèi)任何有關(guān)CPython內(nèi)存管理的源碼分析的技術(shù)文章戈二,基本上會(huì)引用到該圖。但細(xì)心的讀者有否提出個(gè)疑問(wèn)這個(gè)圖憑什么依據(jù)繪制出來(lái)的呢喳资?對(duì)于剛接觸C代碼的程序員可能不太好理解觉吭。
其實(shí)很簡(jiǎn)單,你在pymalloc_alloc函數(shù)或pymalloc_free函數(shù)仆邓,因?yàn)樗鼈兊纳舷挛脑诤瘮?shù)都調(diào)用usedpools數(shù)組鲜滩,只要在函數(shù)結(jié)束的return語(yǔ)句之前伴鳖,嘗試用for循環(huán)配合printf函數(shù)打印usedpools數(shù)組的元素項(xiàng),如下代碼
static inline void*
pymalloc_alloc(void *ctx, size_t nbytes)
{
...
//測(cè)試代碼
for(int j=0;j<30;j++){
printf("usedpools[%d] addr is %ld; usedpools[%d]->nextpool is %ld,
usedpools[%d]->prevpool is %ld\n"
,j,&usedpools[j],j,usedpools[j]->nextpool,j,usedpools[j]->prevpool);
}
return (void *)bp;
}
在編譯后,嘗試在運(yùn)行一次python解釋器徙硅,在運(yùn)行時(shí)隨機(jī)打印前12項(xiàng)的usedpools元素榜聂,請(qǐng)留意觀察如下代碼的一些規(guī)律
Ok,還不明白的話!我們將上面的打印輸出繪制成一個(gè)usedpools數(shù)組的前12項(xiàng)嗓蘑,這回一目了然吧须肆。
我們看看如上圖中有什么規(guī)律可循吧!桩皿!
- 下標(biāo)為偶數(shù)的元素項(xiàng)usedpool[2x]->nextpool始終指向usedpools[2x-2]的內(nèi)存地址豌汇。
- 下標(biāo)為偶數(shù)的元素項(xiàng)usedpool[2x]->prevpool始終指向usedpools[2x-2]的內(nèi)存地址。
- 下標(biāo)為奇數(shù)的元素項(xiàng)usedpool[2x+1]->prevpool始終指向usedpools[2x+1-3] ,即usedpools[2x-2]的內(nèi)存地泄隔。
備注:以上x(chóng)是一個(gè)大于1的正整數(shù),很奇妙的是不論什么索引拒贱,當(dāng)前usedpool元素項(xiàng)的prevpool或nextpool指針的向前偏移單位均為2倍的block尺寸。
我們用一個(gè)源代碼示例簡(jiǎn)述一下CPython如何調(diào)用usedpools數(shù)組中的可用內(nèi)存池佛嬉。例如當(dāng)CPython嘗試為一個(gè)內(nèi)存量為28字節(jié)的Python對(duì)象逻澳,那么CPython會(huì)在底層執(zhí)行如下源代碼Objects/obmalloc.c的第1604行到1634行所示
static inline void*
pymalloc_alloc(void *ctx, size_t nbytes)
{
...
//第一步:
uint size = (uint)(nbytes - 1) >> ALIGNMENT_SHIFT;
poolp pool = usedpools[size + size];
block *bp;
//第二步:比較pool->nextpool和當(dāng)前pool的地址不同表明存在可用的內(nèi)存池
if (LIKELY(pool != pool->nextpool)) {
++pool->ref.count;
bp = pool->freeblock;
assert(bp != NULL);
//第三步
if (UNLIKELY((pool->freeblock = *(block **)bp) == NULL)) {
// Reached the end of the free list, try to extend it.
pymalloc_pool_extend(pool, size);
}
else {
/* There isn't a pool of the right size class immediately
* available: use a free pool.
*/
bp = allocate_from_new_pool(size);
}
return (void *)bp;
}
第一步:這是一個(gè)小型對(duì)象,28字節(jié)換算成size class就是32,那么size class換算成對(duì)應(yīng)的szidx在不同版本的CPython版本中暖呕,會(huì)有不同的差別斜做,見(jiàn)如下圖,Cpython3.6之前的szidx是3,CPython3.7之后的szidx是1
第二步:CPython根據(jù)對(duì)應(yīng)的szidx去訪問(wèn)usedpools,對(duì)于CPython3.6之前的是usedpool[3+3]湾揽,那么usedpools[6]->nextpool指向usedpools[4]的內(nèi)存地址,并從usedpools[4]所指向的內(nèi)存池(pool->freeblock)分配可用的32字節(jié)的塊
對(duì)于CPython3.7之后來(lái)說(shuō)就是usedpools[1+1]陨享,那么usedpools[2]->nextpool自然就指向usedpools[0],并從usedpools[0]所指向的內(nèi)存池(pool->freeblock)分配可用的32字節(jié)的塊钝腺。
第三步:若第二步usedpools若返回的內(nèi)存池pool,并且pool中freeblock鏈表為NULL時(shí)赞厕,就會(huì)觸發(fā)pymalloc_pool_extend(pool, size)函數(shù)艳狐,朝高地址方向偏移pool內(nèi)部nextoffset指針劃分新的塊,并分配該塊皿桑。
static void
pymalloc_pool_extend(poolp pool, uint size)
{
if (UNLIKELY(pool->nextoffset <= pool->maxnextoffset)) {
/* There is room for another block. */
pool->freeblock = (block*)pool + pool->nextoffset;
pool->nextoffset += INDEX2SIZE(size);
*(block **)(pool->freeblock) = NULL;
return;
}
/* Pool is full, unlink from used pools. */
poolp next;
next = pool->nextpool;
pool = pool->prevpool;
next->prevpool = pool;
pool->nextpool = next;
}
內(nèi)存池緩存隊(duì)列的初始化
OK毫目,我們理解usedpools的內(nèi)存模型后,還有個(gè)問(wèn)題诲侮,就是CPython如何初始化我們的usedpools的呢镀虐?當(dāng)CPython啟動(dòng)后,usedpool這個(gè)連續(xù)的內(nèi)存空間并不存在任何指向可用內(nèi)存池的指針沟绪。事實(shí)上CPython采取了延遲分配的策略刮便,當(dāng)確實(shí)開(kāi)始申請(qǐng)小型對(duì)象的內(nèi)存時(shí),CPython才科室構(gòu)建這個(gè)內(nèi)存池绽慈,正如前面所提到的恨旱,當(dāng)申請(qǐng)28個(gè)字節(jié)的內(nèi)存時(shí)辈毯,實(shí)際上是申請(qǐng)32個(gè)字節(jié)的內(nèi)存,假設(shè)CPython3.6之前的版本搜贤,也就找到usedpool[4]中的pool_header指針?biāo)赶虻膬?nèi)存池谆沃,若在對(duì)應(yīng)的位置沒(méi)有找到任何pool_header指針,怎么辦呢仪芒?請(qǐng)回憶一下前一篇所說(shuō)過(guò)arenas對(duì)象唁影,CPython會(huì)從usable_arenas鏈表中的第一個(gè)"可用"的arena對(duì)象中取出一個(gè)pool
更新中....