第6篇CPython內(nèi)存模型架構(gòu)-Layer 2 - 內(nèi)存池緩存陣列

在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代碼的程序員可能不太好理解觉吭。


來(lái)源:《Python源碼剖析:深度探索動(dòng)態(tài)語(yǔ)言核心技術(shù)》第445頁(yè)

其實(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

更新中....

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市掂名,隨后出現(xiàn)的幾起案子据沈,更是在濱河造成了極大的恐慌,老刑警劉巖铆隘,帶你破解...
    沈念sama閱讀 222,104評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件卓舵,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡膀钠,警方通過(guò)查閱死者的電腦和手機(jī)掏湾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)肿嘲,“玉大人融击,你說(shuō)我怎么就攤上這事■撸” “怎么了尊浪?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,697評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)封救。 經(jīng)常有香客問(wèn)我拇涤,道長(zhǎng),這世上最難降的妖魔是什么誉结? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,836評(píng)論 1 298
  • 正文 為了忘掉前任鹅士,我火速辦了婚禮,結(jié)果婚禮上惩坑,老公的妹妹穿的比我還像新娘掉盅。我一直安慰自己,他們只是感情好以舒,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,851評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布趾痘。 她就那樣靜靜地躺著,像睡著了一般蔓钟。 火紅的嫁衣襯著肌膚如雪永票。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 52,441評(píng)論 1 310
  • 那天,我揣著相機(jī)與錄音瓦侮,去河邊找鬼艰赞。 笑死,一個(gè)胖子當(dāng)著我的面吹牛肚吏,可吹牛的內(nèi)容都是我干的方妖。 我是一名探鬼主播,決...
    沈念sama閱讀 40,992評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼罚攀,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼党觅!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起斋泄,我...
    開(kāi)封第一講書(shū)人閱讀 39,899評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤杯瞻,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后炫掐,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體魁莉,經(jīng)...
    沈念sama閱讀 46,457評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,529評(píng)論 3 341
  • 正文 我和宋清朗相戀三年募胃,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了旗唁。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,664評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡痹束,死狀恐怖检疫,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情祷嘶,我是刑警寧澤屎媳,帶...
    沈念sama閱讀 36,346評(píng)論 5 350
  • 正文 年R本政府宣布,位于F島的核電站论巍,受9級(jí)特大地震影響烛谊,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜嘉汰,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,025評(píng)論 3 334
  • 文/蒙蒙 一晒来、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧郑现,春花似錦、人聲如沸荧降。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,511評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)朵诫。三九已至辛友,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背废累。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,611評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工邓梅, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人邑滨。 一個(gè)月前我還...
    沈念sama閱讀 49,081評(píng)論 3 377
  • 正文 我出身青樓日缨,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親掖看。 傳聞我的和親對(duì)象是個(gè)殘疾皇子匣距,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,675評(píng)論 2 359