第6篇CPython内存模型架构-Layer 2 - 内存池缓存
在Python3.x中,Python内部默认的小块内存与大块内存的分界点是512字节,我们知道当小于512字节的内存请求,PyObject_Malloc会在内存池中申请内存,当申请的内存大于512字节,PyObject_Malloc的行为会蜕化为malloc的行为。当与小型对象的内存沈秦,Python会使用arenas所维护的内存空间,那么Python内部对于对于arena的个数是否有闲置?换句话说Python对于这个小块空间内存的尺寸是否闲置,这个取决于用户,Python提供一个编译符号,用于控制是否限制这个内存池的尺寸。
当Python在WITH_MEMORY_LIMITS编译符号打开的背景下进行编译,Python内部的另一个符号会被激活,这个名为SMALL_MEMORY_LIMIT的符号限制了整个内存池的尺寸,同时也就限制了可以创建的arena的个数,在默认情况下,不论是Win32平台,还是unix平台,这个编译符号都没有打开的
当我们申请一个28字节的内存时,Python内部会在内存池寻找一块能满足需求的pool,并从中取出一个block,而不会去需找arena,这实际上事由pool和arena自身的属性确定的,pool有一个size概念的内存管理抽象体,一个pool中的block总是有确定的类型尺寸.pool_header结构体定义中有一个szidx就是指定了对应的pool分配出去的块的最小的块单位-类型尺寸(size class),然而arena没有size idx的概念,这意味着同一个arena,在某个时刻,其托管的内存池集合可能是size class为32字节的内存池,而另一个时刻该内存池可能会被重新划分,变为64字节的block。
我们在讨论单个内存池时,有涉及池状态的概念。这里复习一下
- used:池中至少由一个block已经正在使用,并且至少由一个block还未被使用,这种状态的内存池由CPython的usedpool统一管理
- full状态:pool中所有block都已正在使用,这种状态的pool在arena托管的池集合内,但不再arena的freepools链表中。
- empty状态:pool中的所有状态都未被使用,处于这个状态的pool的集合通过其pool_header结构体的nextpool构成一个链表,这个链表的表头就是arena_object结构体的freepools指针。
解读usedpools数组
Python内部通过使用usedpools数组,维护者所有处于used状态的pool。当申请内存size class为N时,Python会通过usedpools查找到与N对应的size idx可用的内存池,从中分配一个类型尺寸为N的块,我们看看Objects/obmalloc.c源代码的第1101行到1130行定义,其中的NB_SMALL_SIZE_CLASSES标识当前的CPython实现有多少个size class,对于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
由于任意一个usedpools元素项的表达式为PT(x)等价于PTA(x),PTA(x),那么usedpools的中间形式均等价如下
对于CPython 3.6之前的版本,按8字节对齐,上面的usedpools数组形式,等价于以下代码
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)
}
对于CPython3.7之后的版本,按16字节对齐,上面的usedpools数组形式,等价于以下代码
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)
}
好了,从任意一个PTA(x)的元素项,等价于((poolp )((uint8_t )&(usedpools[2(x)]) - 2*sizeof(block *))),其实整个usedpools数组的核心难点就是该PTA(x)的宏等价表达式。
我们不妨使用一些演算的例子,例如我们需要申请一个为28字节的内存,我们通过size class和size indx的换算表,构造一个usedpools的内存模型,这样可以透彻理解usedpools数组的内在含义。
以CPython 3.6之前的为例,由于8字节对齐,那么28字节对应的size calss 是32,对应的szidx是3,对应的换算代码
uint size = (uint)(nbytes - 1) >> ALIGNMENT_SHIFT;
那么看一下源代码Objects/obmalloc.c的第1590行-1610行
static inline void*
pymalloc_alloc(void *ctx, size_t nbytes)
{
...
uint size = (uint)(nbytes - 1) >> ALIGNMENT_SHIFT;
poolp pool = usedpools[size + size];
block *bp;
...
更新中....