09月17, 2020

21. 内存管理漫游-3

前面已经在 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。

image.png

图中灰色部分为实际可用的数据区。此时数据区域还是一整块连续区域,小对象的尺寸范围是 [1,512] Bytes,这时就有一个问题,对内存的需要是不可预料的,各种尺寸的小对象会在不确定的时候申请,并在不确定的时候归还到内存池,假设所有内存尺寸都分配在同一个 pool 下,那么经过一段时间内存池看起来会是这样:

image.png

在内存池中也出现了碎片化,虽然 wanted size 大小的空间大小小于内存池中 free 的空间总和,但是空闲空间不是连续的,就无法直接完成所需要的内存分配,此时必定需要某种内存整理策略,而已分配的内存又不能直接移动,否则指向这些内存的指针就失效了,问题越来越复杂了!!内存碎片化的问题随之时间推移会越来越严重,如果不妥善处理,一段时间之后,整个内存池甚至连一些很小的连续内存都分配不出来,基本上算是无效内存池。

因此,以何种策略管理第一张图中的数据区域非常重要,Python 的策略是在一个内存池中,数据区域按照特定大小分割成相等的连续 block,每次分配和归还,都只能以单个 block 进行。

image.png

由于内存是按照 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 满了还需要扩容,这些问题会在下一步解决。

本文链接:http://www.thinkinpython.com/post/deep_python_vm_21.html

-- EOF --