python内存模型_内存篇3:CPython的内存管理架构-L2-块
本篇用到了C/C++的內存對齊的基礎知識,我已經假定你有C/C++內存管理的相關基礎。
我們在前一篇的流程圖中留下了兩個黑箱子,會涉及到內存模型第一層以上的其他話題,回顧下面關于第一層面向類型的內存API流程執行圖。本篇要討論其中一個黑箱就是何為物?
首先PyMem_這些函數族,在邏輯上是CPython內存模型架構的第1層,
再次,_PyObject_函數族一個銜接第1層和第2層的,銜接函數接口
pymalloc_alloc函數壓根就不是分配器(不知道為何官方冠以默認分配器之名),更確切地說是一個調度函數,將來自外部CPython其他內部對象的內存空間請求是往第2層還是往第1層轉發,顯然當需要分配大于512字節時,調用前上圖提到的PyMem_Raw前綴的函數族。
那么,我們不妨將前一篇內存模型架構圖和上面的內存函數接口執行流程圖結合一起,我們可以得到一個更為清晰的CPython內存模型架構圖,圖中提到aranas和pool是本篇需要提及的難點,
Layer 1與Layer 2的內存APIs的交互
不過在深入了解這個CPython的內存策略前,我們需要引入兩個CPython的專業術語,CPython根據內存分配的尺寸的閥值512字節可以分為,對Python對象做如下分類:大于512字節的Python對象,稱為大型對象(Big),而Arenas對象的尺寸為256KB就是CPython中大型對象因此Arenas對象的內存分配,CPython會選擇調用PyMem_RawMalloc()或PyMem_RawRealloc()為其分配內存,換句話就是通過第0層去調用C庫的malloc分配器,因此C底層的malloc分配器是僅供給arenas對象使用的。
少于或等于512字節的Python對象,稱為小型對象(Small),小型對象的內存請求按該對象的類型尺寸分組,這些分組按8個字節對齊,由于返回的地址必須有效對齊。這些類型尺寸的對象的內存請求由4KB的內存池提供內存分配,當然前提是該內存池有閑置的塊。
內存模型的第2層提到的PyObject_函數族,如下所示,它們位于Objects/obmalloc.c的第679行和第710行,具體的邏輯沒必要好說,跟前篇提到內存函數接口是一致的。
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;
}
我們這里的重點是要遺留的一個關鍵問題的默認的Python內存分配器,遺留的一些代碼細節,我們先看看代碼細節pymalloc_alloc位于源文件Objects/obmalloc.c的第1608行開始開始的代碼細節。見下圖紅色標出的一些C代碼。
上面的代碼細節大意邏輯第一步:檢索數組usepools中與申請的內存尺寸量相關的某個usepools元素,就是我們在上文插圖(Layer 1與Layer 2的內存APIs的交互) 提到的pool,
第二步:在池中找到可用的內存塊(bp=pool->freeblock),若找到舊返回該內存塊,若找不到池中空閑的內存塊就執行pymalloc_pool_extend函數。
第三步:若第一步中連可用的pool(第1612行)都找不到,就執行 allocate_from_new_pool函數
顯然默認的Python內存分配器是直接驅動內存池,間接管理內存池的驅動函數。我們在代碼中提取一些問題,它們就是本文后續隨筆解答的一系列問題。,目前在本篇,我們稍微放下。第1609行的 usedpools是什么?poolp是什么數據類型?
第1610行的block是數據類型?
函數pymalloc_pool_extend(pool,size)的具體邏輯是什么?
allocate_from_new_pool(size)的具體邏輯是什么?
CPython的內存分配策略
CPython的內存管理策略,分3個不同級別的對象,分別是Arenas->pool->block,我先用一個思維導圖,讓你腦海中建立這三個對象的層次關系,讀者可以先通過下圖來初步理解這三個對象。這也是內存模型架構第2層中最為復雜堆內存托管邏輯。
Arenas->pool->block堆內存托管模型每個Arenas對象包裝包含64個內存池,每個Arenas固定大小為256KB,并且該對象頭部用兩個struct area_object類型的指針在堆中構成Arenas對象的雙重鏈表。
每個內存池(Pool),固有尺寸為4KB,每個內存池包含尺寸相同的邏輯塊,并且并且該對象頭部用兩個struct pool_header類型的指針構成pool對象的雙重鏈表。
塊是封裝Python對象的基本單位,對于Areas對象來說都按8字節的塊來劃分PyMem已分配的所有堆內存(備注:切入點1)。
塊(Block)
CPython的內存管理策略中,首先定義邏輯上的“塊”,并且用8字節對齊的方式確定塊的尺寸,換句話說塊的尺寸可以看作8的倍數那么大,例如你創建來一個25字節的Python對象,25字節不是8字節的倍數,那么CPython運行時系統會根據內存對齊的原則為該Python對象額外添加7個填充字節,就湊夠32字節(8的倍數),更明確地說,對于一個實際尺寸位于25~32字節這個區間的任意Python對象,都能放入一個32字節的邏輯塊中,
那么如此類推,我們在得到512字節以內,不同小型對象(Small)的內存請求在內存對齊后的內存塊分配表。
小型對象的內存塊分配表
事實上,我們所說的塊,它的基本單位是8個字節,而對于CPython語義中,有著不同尺寸的block。對于少于512字節的任意Python對象的內存尺寸的分配,不同內存尺寸有對應的按8字節對齊后的塊尺寸對應,w如上表所示的第2列中的8的倍數稱為size class(類型尺寸),每種size class(類型尺寸)都由一個索引與其對應,我們稱這些索引是size class index,由于所有塊的尺寸是8字節對齊
CPython 3.6 之前 和 CPython 3.7之后 對內存塊有了一些調整,對于CPython3.6之前的,我們說上表都是成立的,我們查看一下,具體鏈接https://github.com/python/cpython/blob/3.6/Objects/obmalloc.c
CPython 3.7的內存塊對齊方式基于8個字節
目前網上很多同類型文章是基于CPython2.5或2.7版本為參考來理解CPython3.x的源代碼,有個細節此類文章沒有提到,那就是Objects/obmalloc.c有個細節沒有詳細提到的,那新版本的CPython3.7之后的小型對象的內存塊分配表是就一定要8字節為基準嗎?不一定!來看看關鍵的宏INDEX2SIZE(i),下面代碼位于Objects/obmalloc.c的第846行到855行。
上面代碼的宏SIZE_OF_P其實指代的是sizeof (void*) ,該宏定義在pyconfig.h的頭文件中,CPython3.9默認指定SIZE_OF_P宏常量就為8,
也就是說對于CPython3.7之后的版本,小型對象的內存分配的基準是16字節對齊的,而不是8字節。這里我們嘗試調用這個宏INDEX2SIZE(I),得到一些有趣的結果,可以查看如下測試代碼(該測試代碼中的宏定義是從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;
}
我們看看運行結果,基于16字節的size class,的size class index是0,如此類推直到512字節
我們對上面的結果整理一下,會得到下面基于16字節對齊的小型對象的內存塊分配表
基于16字節對齊的小型對象的內存塊分配表
總結一個簡單的公式size_class_idx=(size_class / ALIGNMENT)-1
小結:
本篇主要討論了CPython內存模型架構第2層中,小型對象(小于512字節的對象)的內存分配原理的一個重要的概念block,以及什么是size class和size class index,那你是否思考過為什么在CPython 3.7之后,CPython的開發團隊為何要將內存塊的對齊基準從8字節調整到16字節呢?有興趣的話,可以參考一下這個鏈接https://github.com/python/cpython/pull/12850,我這里就不細說啦。
總結
以上是生活随笔為你收集整理的python内存模型_内存篇3:CPython的内存管理架构-L2-块的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 百度云下载加速
- 下一篇: 1月28日云栖精选夜读 | 终于等到你!