IT狗工作室Python中文社区

第6篇CPython内存模型架构-Layer 2 - 内存池缓存

2020-06-21  本文已影响0人  铁甲万能狗

在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。

我们在讨论单个内存池时,有涉及池状态的概念。这里复习一下

解读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;
    ...

更新中....

上一篇下一篇

猜你喜欢

热点阅读