python内存模型_内存篇3:CPython的内存管理架构-L2-块
本篇用到了C/C++的內(nèi)存對(duì)齊的基礎(chǔ)知識(shí),我已經(jīng)假定你有C/C++內(nèi)存管理的相關(guān)基礎(chǔ)。
我們?cè)谇耙黄牧鞒虉D中留下了兩個(gè)黑箱子,會(huì)涉及到內(nèi)存模型第一層以上的其他話題,回顧下面關(guān)于第一層面向類型的內(nèi)存API流程執(zhí)行圖。本篇要討論其中一個(gè)黑箱就是何為物?
首先PyMem_這些函數(shù)族,在邏輯上是CPython內(nèi)存模型架構(gòu)的第1層,
再次,_PyObject_函數(shù)族一個(gè)銜接第1層和第2層的,銜接函數(shù)接口
pymalloc_alloc函數(shù)壓根就不是分配器(不知道為何官方冠以默認(rèn)分配器之名),更確切地說是一個(gè)調(diào)度函數(shù),將來自外部CPython其他內(nèi)部對(duì)象的內(nèi)存空間請(qǐng)求是往第2層還是往第1層轉(zhuǎn)發(fā),顯然當(dāng)需要分配大于512字節(jié)時(shí),調(diào)用前上圖提到的PyMem_Raw前綴的函數(shù)族。
那么,我們不妨將前一篇內(nèi)存模型架構(gòu)圖和上面的內(nèi)存函數(shù)接口執(zhí)行流程圖結(jié)合一起,我們可以得到一個(gè)更為清晰的CPython內(nèi)存模型架構(gòu)圖,圖中提到aranas和pool是本篇需要提及的難點(diǎn),
Layer 1與Layer 2的內(nèi)存APIs的交互
不過在深入了解這個(gè)CPython的內(nèi)存策略前,我們需要引入兩個(gè)CPython的專業(yè)術(shù)語,CPython根據(jù)內(nèi)存分配的尺寸的閥值512字節(jié)可以分為,對(duì)Python對(duì)象做如下分類:大于512字節(jié)的Python對(duì)象,稱為大型對(duì)象(Big),而Arenas對(duì)象的尺寸為256KB就是CPython中大型對(duì)象因此Arenas對(duì)象的內(nèi)存分配,CPython會(huì)選擇調(diào)用PyMem_RawMalloc()或PyMem_RawRealloc()為其分配內(nèi)存,換句話就是通過第0層去調(diào)用C庫的malloc分配器,因此C底層的malloc分配器是僅供給arenas對(duì)象使用的。
少于或等于512字節(jié)的Python對(duì)象,稱為小型對(duì)象(Small),小型對(duì)象的內(nèi)存請(qǐng)求按該對(duì)象的類型尺寸分組,這些分組按8個(gè)字節(jié)對(duì)齊,由于返回的地址必須有效對(duì)齊。這些類型尺寸的對(duì)象的內(nèi)存請(qǐng)求由4KB的內(nèi)存池提供內(nèi)存分配,當(dāng)然前提是該內(nèi)存池有閑置的塊。
內(nèi)存模型的第2層提到的PyObject_函數(shù)族,如下所示,它們位于Objects/obmalloc.c的第679行和第710行,具體的邏輯沒必要好說,跟前篇提到內(nèi)存函數(shù)接口是一致的。
void *
PyObject_Malloc(size_t size)
{
/* see PyMem_RawMalloc() */
if (size > (size_t)PY_SSIZE_T_MAX)
return NULL;
return _PyObject.malloc(_PyObject.ctx, size);
}
void *
PyObject_Calloc(size_t nelem, size_t elsize)
{
/* see PyMem_RawMalloc() */
if (elsize != 0 && nelem > (size_t)PY_SSIZE_T_MAX / elsize)
return NULL;
return _PyObject.calloc(_PyObject.ctx, nelem, elsize);
}
void *
PyObject_Realloc(void *ptr, size_t new_size)
{
/* see PyMem_RawMalloc() */
if (new_size > (size_t)PY_SSIZE_T_MAX)
return NULL;
return _PyObject.realloc(_PyObject.ctx, ptr, new_size);
}
void
PyObject_Free(void *ptr)
{
_PyObject.free(_PyObject.ctx, ptr);
}
void
PyObject_GetArenaAllocator(PyObjectArenaAllocator *allocator)
{
*allocator = _PyObject_Arena;
}
void
PyObject_SetArenaAllocator(PyObjectArenaAllocator *allocator)
{
_PyObject_Arena = *allocator;
}
我們這里的重點(diǎn)是要遺留的一個(gè)關(guān)鍵問題的默認(rèn)的Python內(nèi)存分配器,遺留的一些代碼細(xì)節(jié),我們先看看代碼細(xì)節(jié)pymalloc_alloc位于源文件Objects/obmalloc.c的第1608行開始開始的代碼細(xì)節(jié)。見下圖紅色標(biāo)出的一些C代碼。
上面的代碼細(xì)節(jié)大意邏輯第一步:檢索數(shù)組usepools中與申請(qǐng)的內(nèi)存尺寸量相關(guān)的某個(gè)usepools元素,就是我們?cè)谏衔牟鍒D(Layer 1與Layer 2的內(nèi)存APIs的交互) 提到的pool,
第二步:在池中找到可用的內(nèi)存塊(bp=pool->freeblock),若找到舊返回該內(nèi)存塊,若找不到池中空閑的內(nèi)存塊就執(zhí)行pymalloc_pool_extend函數(shù)。
第三步:若第一步中連可用的pool(第1612行)都找不到,就執(zhí)行 allocate_from_new_pool函數(shù)
顯然默認(rèn)的Python內(nèi)存分配器是直接驅(qū)動(dòng)內(nèi)存池,間接管理內(nèi)存池的驅(qū)動(dòng)函數(shù)。我們?cè)诖a中提取一些問題,它們就是本文后續(xù)隨筆解答的一系列問題。,目前在本篇,我們稍微放下。第1609行的 usedpools是什么?poolp是什么數(shù)據(jù)類型?
第1610行的block是數(shù)據(jù)類型?
函數(shù)pymalloc_pool_extend(pool,size)的具體邏輯是什么?
allocate_from_new_pool(size)的具體邏輯是什么?
CPython的內(nèi)存分配策略
CPython的內(nèi)存管理策略,分3個(gè)不同級(jí)別的對(duì)象,分別是Arenas->pool->block,我先用一個(gè)思維導(dǎo)圖,讓你腦海中建立這三個(gè)對(duì)象的層次關(guān)系,讀者可以先通過下圖來初步理解這三個(gè)對(duì)象。這也是內(nèi)存模型架構(gòu)第2層中最為復(fù)雜堆內(nèi)存托管邏輯。
Arenas->pool->block堆內(nèi)存托管模型每個(gè)Arenas對(duì)象包裝包含64個(gè)內(nèi)存池,每個(gè)Arenas固定大小為256KB,并且該對(duì)象頭部用兩個(gè)struct area_object類型的指針在堆中構(gòu)成Arenas對(duì)象的雙重鏈表。
每個(gè)內(nèi)存池(Pool),固有尺寸為4KB,每個(gè)內(nèi)存池包含尺寸相同的邏輯塊,并且并且該對(duì)象頭部用兩個(gè)struct pool_header類型的指針構(gòu)成pool對(duì)象的雙重鏈表。
塊是封裝Python對(duì)象的基本單位,對(duì)于Areas對(duì)象來說都按8字節(jié)的塊來劃分PyMem已分配的所有堆內(nèi)存(備注:切入點(diǎn)1)。
塊(Block)
CPython的內(nèi)存管理策略中,首先定義邏輯上的“塊”,并且用8字節(jié)對(duì)齊的方式確定塊的尺寸,換句話說塊的尺寸可以看作8的倍數(shù)那么大,例如你創(chuàng)建來一個(gè)25字節(jié)的Python對(duì)象,25字節(jié)不是8字節(jié)的倍數(shù),那么CPython運(yùn)行時(shí)系統(tǒng)會(huì)根據(jù)內(nèi)存對(duì)齊的原則為該P(yáng)ython對(duì)象額外添加7個(gè)填充字節(jié),就湊夠32字節(jié)(8的倍數(shù)),更明確地說,對(duì)于一個(gè)實(shí)際尺寸位于25~32字節(jié)這個(gè)區(qū)間的任意Python對(duì)象,都能放入一個(gè)32字節(jié)的邏輯塊中,
那么如此類推,我們?cè)诘玫?12字節(jié)以內(nèi),不同小型對(duì)象(Small)的內(nèi)存請(qǐng)求在內(nèi)存對(duì)齊后的內(nèi)存塊分配表。
小型對(duì)象的內(nèi)存塊分配表
事實(shí)上,我們所說的塊,它的基本單位是8個(gè)字節(jié),而對(duì)于CPython語義中,有著不同尺寸的block。對(duì)于少于512字節(jié)的任意Python對(duì)象的內(nèi)存尺寸的分配,不同內(nèi)存尺寸有對(duì)應(yīng)的按8字節(jié)對(duì)齊后的塊尺寸對(duì)應(yīng),w如上表所示的第2列中的8的倍數(shù)稱為size class(類型尺寸),每種size class(類型尺寸)都由一個(gè)索引與其對(duì)應(yīng),我們稱這些索引是size class index,由于所有塊的尺寸是8字節(jié)對(duì)齊
CPython 3.6 之前 和 CPython 3.7之后 對(duì)內(nèi)存塊有了一些調(diào)整,對(duì)于CPython3.6之前的,我們說上表都是成立的,我們查看一下,具體鏈接https://github.com/python/cpython/blob/3.6/Objects/obmalloc.c
CPython 3.7的內(nèi)存塊對(duì)齊方式基于8個(gè)字節(jié)
目前網(wǎng)上很多同類型文章是基于CPython2.5或2.7版本為參考來理解CPython3.x的源代碼,有個(gè)細(xì)節(jié)此類文章沒有提到,那就是Objects/obmalloc.c有個(gè)細(xì)節(jié)沒有詳細(xì)提到的,那新版本的CPython3.7之后的小型對(duì)象的內(nèi)存塊分配表是就一定要8字節(jié)為基準(zhǔn)嗎?不一定!來看看關(guān)鍵的宏INDEX2SIZE(i),下面代碼位于Objects/obmalloc.c的第846行到855行。
上面代碼的宏SIZE_OF_P其實(shí)指代的是sizeof (void*) ,該宏定義在pyconfig.h的頭文件中,CPython3.9默認(rèn)指定SIZE_OF_P宏常量就為8,
也就是說對(duì)于CPython3.7之后的版本,小型對(duì)象的內(nèi)存分配的基準(zhǔn)是16字節(jié)對(duì)齊的,而不是8字節(jié)。這里我們嘗試調(diào)用這個(gè)宏INDEX2SIZE(I),得到一些有趣的結(jié)果,可以查看如下測(cè)試代碼(該測(cè)試代碼中的宏定義是從CPython截取于源碼文件Objects/obmalloc.c)
#include
#define uint unsigned int#define SIZEOF_VOID_P 8
#if SIZEOF_VOID_P > 4#define ALIGNMENT 16/* must be 2^N */#define ALIGNMENT_SHIFT 4#else#define ALIGNMENT 8/* must be 2^N */#define ALIGNMENT_SHIFT 3#endif
#define INDEX2SIZE(I) (((uint)(I) + 1) << ALIGNMENT_SHIFT)#define _Py_SIZE_ROUND_UP(n, a) (((size_t)(n) + \(size_t)((a) - 1)) & ~(size_t)((a) - 1))#define POOL_SIZE (4*1024)#define POOL_OVERHEAD _Py_SIZE_ROUND_UP(sizeof(struct pool_header), ALIGNMENT)
int main()
{
unsigned int size_class=0;
for(int i=0;i<=63;i++){
size_class=INDEX2SIZE(i);
if(size_class>512){
break;
}
printf("size-class: %d,size-class-idx:%d\n",size_class,i);
}
return 0;
}
我們看看運(yùn)行結(jié)果,基于16字節(jié)的size class,的size class index是0,如此類推直到512字節(jié)
我們對(duì)上面的結(jié)果整理一下,會(huì)得到下面基于16字節(jié)對(duì)齊的小型對(duì)象的內(nèi)存塊分配表
基于16字節(jié)對(duì)齊的小型對(duì)象的內(nèi)存塊分配表
總結(jié)一個(gè)簡(jiǎn)單的公式size_class_idx=(size_class / ALIGNMENT)-1
小結(jié):
本篇主要討論了CPython內(nèi)存模型架構(gòu)第2層中,小型對(duì)象(小于512字節(jié)的對(duì)象)的內(nèi)存分配原理的一個(gè)重要的概念block,以及什么是size class和size class index,那你是否思考過為什么在CPython 3.7之后,CPython的開發(fā)團(tuán)隊(duì)為何要將內(nèi)存塊的對(duì)齊基準(zhǔn)從8字節(jié)調(diào)整到16字節(jié)呢?有興趣的話,可以參考一下這個(gè)鏈接https://github.com/python/cpython/pull/12850,我這里就不細(xì)說啦。
總結(jié)
以上是生活随笔為你收集整理的python内存模型_内存篇3:CPython的内存管理架构-L2-块的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 百度云下载加速
- 下一篇: 小李飞刀:用python刷题ing...