前面已经在 pymalloc_alloc 中找到了小内存分配的代码,回顾一下,除了 size 为 0 或者大于 512 Bytes 的内存,都算作小内存,尝试在内存池中分配。按照内存池的设计思路,会预先申请一个 “足够” 大的连续空间备用,所有的小内存则在这个空间里分割出来。Python 中用一个结构体 pool_head 来记录这一大块内存的信息。哦,另外提一句,Python 中这个 “足够” 大是 4KB,正好是操作系统的内存分页大小,这样做的好处是方便操作系统按页换出,提升效率。
struct pool_header {
union { block *_padding;
uint count; } ref; /* number of allocated blocks */
block *freeblock; /* pool's free list head */
struct pool_header *nextpool; /* next pool of this size class */
struct pool_header *prevpool; /* previous pool "" */
uint arenaindex; /* index into arenas of base adr */
uint szidx; /* block size class index */
uint nextoffset; /* bytes to virgin block */
uint maxnextoffset; /* largest valid nextoffset */
};
pool_header 结构直接位于 4KB 大的内存池头部,其后是内存池的有效区域,头部是专门用于保存内存池相关信息的,不能用作数据区。这些头部信息中,目前我们仅关注 nextoffset、maxnextoffset、freeblock。
图中灰色部分为实际可用的数据区。此时数据区域还是一整块连续区域,小对象的尺寸范围是 [1,512] Bytes,这时就有一个问题,对内存的需要是不可预料的,各种尺寸的小对象会在不确定的时候申请,并在不确定的时候归还到内存池,假设所有内存尺寸都分配在同一个 pool 下,那么经过一段时间内存池看起来会是这样:
在内存池中也出现了碎片化,虽然 wanted size 大小的空间大小小于内存池中 free 的空间总和,但是空闲空间不是连续的,就无法直接完成所需要的内存分配,此时必定需要某种内存整理策略,而已分配的内存又不能直接移动,否则指向这些内存的指针就失效了,问题越来越复杂了!!内存碎片化的问题随之时间推移会越来越严重,如果不妥善处理,一段时间之后,整个内存池甚至连一些很小的连续内存都分配不出来,基本上算是无效内存池。
因此,以何种策略管理第一张图中的数据区域非常重要,Python 的策略是在一个内存池中,数据区域按照特定大小分割成相等的连续 block,每次分配和归还,都只能以单个 block 进行。
由于内存是按照 block 分配,所以即使长时间运行,内存也不会出现碎片化。那么新的问题是这个 block 设置多大合适?前面提到小对象的界限是 512 Bytes,如果将 block 大小设置为 512 Bytes,那么即使很小的对象也需要占用整个 block,造成内存浪费,如果设置为更小的值显然也不合适,较大的对象就无法分配了。Python 的策略是创建一系列内存池,不同内存池的 block 不一样大(上面图中第二个就比第一个内存池的 pool 大一些)。
Python 3.8 中,从最小的 block 尺寸到最大的尺寸有这样的序列:
申请大小 | block 大小 | size index |
---|---|---|
1-8 | 8 | 0 |
9-16 | 16 | 1 |
17-24 | 24 | 2 |
25-32 | 32 | 3 |
33-40 | 40 | 4 |
41-48 | 48 | 5 |
49-56 | 56 | 6 |
57-64 | 64 | 7 |
65-72 | 72 | 8 |
... | ... | ... |
497-504 | 504 | 62 |
505-512 | 512 | 63 |
显然这些 block 的序列是 8 * N,以 8 为基准是为了实现内存对齐。这样创建多种规格的 pool,就可以兼顾空间和时间效率,当申请某个 size 的内存,Python 会将这个 size 向上按 8 取整,在对应的 block 中分配内存。这里也隐含着一个事实,pool 可以按照需要设置 block 的大小,那么一旦确定,这个 pool 的内存都只能按照 block 管理了。不同 pool 中的 block 数量也是不一样的,相当于 (4K - sizeof(pool_header) - sizeof(padding))/ block_size。
内存池的结构搞定了,内存管理的复杂性在于,申请和释放都是随机的,那些归还的 block 还要能回收利用,pool 满了还需要扩容,这些问题会在下一步解决。