本篇用到了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,我这里就不细说啦。