>  기사  >  백엔드 개발  >  Python 메모리 관리자는 풀링 기술을 어떻게 구현합니까?

Python 메모리 관리자는 풀링 기술을 어떻게 구현합니까?

WBOY
WBOY앞으로
2023-05-22 19:03:021195검색

머리말

Python의 모든 것은 객체입니다. 이러한 객체의 메모리는 런타임 시 힙에 동적으로 할당됩니다. Python 가상 머신에서 사용하는 스택도 힙에서 시뮬레이션됩니다. 모든 것이 객체이기 때문에 Python 프로그램을 실행하는 동안 객체가 매우 자주 생성되고 해제됩니다. 운영 체제에서 메모리를 적용하거나 해제할 때마다 malloc() 및 free()를 사용하면 성능에 영향을 미칩니다. 모두, 이 함수는 결국 시스템 호출을 만들어 컨텍스트 전환을 유발합니다.

실제로 핵심은 한 번에 연속적인 메모리 공간 배치를 위한 운영 체제에 적용되는 기술입니다. 객체를 생성해야 할 때마다 이 배치 공간에서 여유 메모리 블록을 찾아 할당합니다. 객체가 해제되면 해당 메모리 블록이 사용 가능한 것으로 표시되므로 프로그램의 전체 객체 메모리 공간이 안정적인 한 운영 체제에서 매번 메모리를 신청하고 해제할 필요가 없습니다. Python이 운영 체제에서 메모리를 적용하고 해제하는 것은 매우 낮습니다. 이 솔루션에 대해 잘 알고 계시나요? 데이터베이스 연결 풀에도 비슷한 아이디어가 있습니다. 일반적으로 백엔드 애플리케이션도 데이터베이스와 미리 여러 ​​개의 연결을 설정하며, SQL이 실행될 때마다 데이터베이스와 상호 작용할 수 있는 연결이 발견되면 해당 연결이 연결 풀로 반환됩니다. 연결에 시간이 오래 걸립니다. 사용하지 않으면 연결 풀에서 연결을 해제합니다. 본질적으로 이는 공간을 시간으로 교환하고, 너무 크지 않은 일부 메모리를 소비하고, 메모리 응용 프로그램 및 TCP 연결 설정과 같이 시간이 많이 걸리는 작업의 빈도를 줄이고, 프로그램의 전반적인 실행 속도를 향상시키는 것입니다.

메모리 계층 구조

Python의 메모리 관리자는 메모리를 큰 것부터 작은 것까지 세 가지 수준(아레나, 풀, 블록)으로 나눕니다. Arena는 메모리 관리자가 운영 체제에서 적용하기 위해 malloc() 또는 calloc()을 직접 호출하는 큰 메모리 조각입니다. Python에서 객체의 생성과 해제는 모두 arena에서 할당되고 재활용됩니다. 아레나는 여러 개의 풀로 나누어지며, 각 풀은 여러 개의 동일한 크기의 블록으로 나누어 메모리가 할당될 때마다 특정 풀에서 사용 가능한 블록을 선택하여 반환합니다. 서로 다른 풀은 서로 다른 크기의 블록을 가질 수 있지만 동일한 풀의 블록 크기는 동일해야 합니다.

Python 메모리 관리자는 풀링 기술을 어떻게 구현합니까?

Arena, Pool 및 블록의 크기는 32비트 시스템과 64비트 시스템에서 다릅니다. 블록 크기는 ALIGNMENT의 배수여야 하며 최대값은 512바이트입니다. 다른 기계.

32비트 머신 64비트 머신
아레나 크기 256KB 1MB
풀 크기 4KB 16KB
ALIGNMENT 8 B 16 B

64비트 시스템을 예로 들어 보겠습니다. 가능한 모든 블록 크기는 16, 32, 48...496, 512입니다. 각 크기는 작은 것부터 큰 것까지, 0, 1, 2... 30의 크기 클래스에 해당합니다. , 31. 메모리를 할당할 때 요청한 크기보다 작지 않고 가장 작은 여유 블록을 찾아야 합니다. 블록 크기 등급 지정의 목적은 다양한 크기의 메모리 요청에 적응하고, 메모리 조각 생성을 줄이고, 경기장 활용도를 높이는 것입니다.

메모리 관리 로직

아레나, 풀, 블록의 개념을 이해한 후에는 필요한 메모리 크기가 n바이트인 경우

  • n > 512인 경우 fallback은 malloc( )입니다. , 최대 블록 크기는 512바이트이므로

  • 그렇지 않으면 n보다 작지 않은 최소 블록 크기를 계산합니다(예: n=105). 64비트 시스템의 최소 블록 크기는 112

  • 에서 크기와 두 번째는 동일한 메모리 풀에 블록을 할당합니다. 사용 가능한 Pool이 없으면 사용 가능한 Arena에서 Pool을 할당합니다. 사용 가능한 Arena가 없으면 malloc()을 사용하여 운영체제에서 새 Arena를 신청합니다

메모리 해제 논리는 다음과 같습니다

  • 먼저 해제 여부를 결정합니다. 메모리가 Python 메모리 관리자에 의해 할당되었습니까? 직접 반환되지 않으면 해제할 메모리에 해당하는 블록과 풀을 찾아서 블록을 풀에 반환하고 다음을 위해 남겨둡니다. 할당이 해제되면 블록이 위치한 경기장은 그 자체를 제외하고는 무료입니다. 그런 다음 블록이 반환된 후에는 free()를 사용하여 경기장을 해제하고 운영 체제로 반환할 수 있습니다.

    Python에서 객체는 일반적으로 크기가 크지 않고 수명주기가 짧기 때문에 일단 Arena를 신청하면 객체의 할당 및 해제가 대부분 Arena에서 수행되므로 효율성이 향상됩니다.
  • 위에서 Python 메모리 관리자의 핵심 로직을 명확하게 설명했지만, 메모리 할당 시 블록 크기에 따라 해당 풀을 찾는 방법, 이러한 풀이 각각의 풀과 어떻게 연관되어 있는지 등 일부 세부적인 문제는 아직 해결되지 않았습니다. 기타 메모리가 해제될 때 Python 메모리 관리자가 해제할 메모리를 할당하는지 확인하는 방법 등 다음은 소스 코드를 결합하여 메모리 할당 및 해제 로직을 세부적으로 확장한 것입니다.
  • 먼저 메모리 레이아웃과 아레나 및 풀의 해당 데이터 구조를 소개한 다음 64비트 머신을 예로 들어 pymalloc_alloc() 및 pymalloc_free()의 로직을 자세히 분석합니다.

  • 메모리 레이아웃 및 해당 데이터 구조
  • Arena


arena는 1MB, 풀은 16KB, 풀은 경기장에 인접해 있으며 경기장에는 최대 1MB/16KB = 64개의 풀이 있습니다. Python 메모리 관리자는 각 풀의 첫 번째 주소가 POOL_SIZE의 정수 배수가 되도록 경기장에 있는 첫 번째 풀의 첫 번째 주소를 정렬합니다. 주소를 쉽게 계산할 수 있으며, 이 기능은 메모리가 해제될 때 사용됩니다. POOL_SIZE는 32비트 시스템에서 4KB이고 64비트 시스템에서 16KB입니다. 이것의 또 다른 장점은 각 풀이 하나 이상의 물리적 페이지에 정확하게 포함되어 메모리 액세스 효율성이 향상된다는 것입니다. 위 그림의 회색 메모리 블록은 정렬을 위해 폐기됩니다. malloc()에 의해 할당된 메모리의 첫 번째 주소가 정렬되면 풀 수는 64이고, 그렇지 않으면 63입니다. 물론, Arena는 처음에 모든 풀을 나누지는 않지만, 사용 가능한 풀이 없을 때 새로운 풀을 나눕니다. 모든 풀이 나누어지면 위 그림과 같은 레이아웃이 됩니다.

각 경기장은 struct arena_object 구조로 표시되지만 모든 struct arena_object에 해당 경기장이 있는 것은 아닙니다. 경기장이 해제된 후에도 해당 struct arena_object가 계속 유지되기 때문입니다. 경기장에 해당하지 않는 이러한 struct arena_object는 단일 링크에 저장됩니다. 다음번에 경기장을 할당할 때 사용할 수 있는 미사용_arena_objects 목록을 작성하세요. struct arena_object에 해당 arena가 있고 해당 arena에 할당할 수 있는 풀이 있는 경우 struct arena_object는 usable_arenas에 이중 연결 리스트에 저장됩니다. 동시에 모든 struct arena_objects는 배열 arenas에 저장됩니다. 해당 경기장이 있는지 여부. usable_arenas의 아레나는 포함된 여유 풀 수에 따라 작은 것부터 큰 것까지 정렬됩니다. 이 정렬은 다음에 풀이 할당될 때 더 많은 메모리를 사용한 아레나가 먼저 사용된 다음 나중에 메모리가 할당될 때 순위가 매겨지게 됩니다. 여유 메모리가 더 많은 아레나는 완전히 유휴 상태가 될 가능성이 높으므로 메모리 공간을 해제하고 이를 운영 체제에 반환하여 전체 메모리 소비를 줄입니다.

struct arena_object의 구조와 각 필드의 의미는 다음과 같습니다

struct arena_object {
    uintptr_t address; // 指向 arena 的起始地址,如果当前 arena_object 没有对应的 arena 内存则 address = 0
    block* pool_address; // pool 需要初始化之后才能使用,pool_address 指向的地址可以用来初始化一个 pool 用于分配
    int nfreepools; // arena 中目前可以用来分配的 pool 的数量
    uint ntotalpools; // arena 中 pool 的总数量,64 或 63
    struct pool_header* freepools; // arena 中可以分配的 pool 构成一个单链表,freepools 指针是单链表的第一个节点
    struct arena_object* nextarena; // 在 usable_arenas 或 unused_arena_objects 指向下一个节点
    struct arena_object* prevarena; // 在 usable_arenas 中指向上一个节点
}

Pool

Python 메모리 관리자는 풀링 기술을 어떻게 구현합니까?

pool 的内部等分成多个大小相等的 block,与 arena 一样,也有一个数据结构 struct pool_header 用来表示 pool。与 arena 不同的是,struct pool_header 位于 pool 的内部,在最开始的一段内存中,紧接之后的是第一个 block,为了让每个 block 的地址都能对齐机器访问内存的步长,可能需要在 struct pool_header 和第一个 block 之间做一些 padding,图中灰色部分所示。这部分 padding 不一定存在,在 64 位机器上 sizeof(struct pool_header) 为 48 字节,本来就已经对齐了,后面就直接跟第一个 block,中间没有 padding。即使如此,pool 最后的一小块内存也可能用不上,上图中下面的灰色部分所示,因为每个 pool 中 block 大小是相等的,假设 block 为 64 字节,一个 pool 中可以分出 255 个 block,前面 48 字节存储 struct pool_header,后面 16 字节用不上,当然如果 block 大小为 48 字节或 16 字节那么整个 pool 就会被完全利用上。同 arena 一样,pool 一开始不是把所有的 block 全部划分出来,而是在没有可用 block 的时候才回去新划分一个,在所有的 block 全部划分之后 pool 的布局如上图所示。

接下来看看 struct pool_header 的结构

struct pool_header {
    union { block *_padding;
            uint count; } ref; // 当前 pool 中已经使用的 block 数量,共用体中只有 count 字段有意义,_padding 是为了让 ref 字段占 8 个字节,这个特性在 usedpools 初始化的时候有用
    block *freeblock; // pool 中可用来进行分配的 block 单链表的头指针
    struct pool_header *nextpool; // 在 arena_object.freepools 或 usedpools 中指向下一个 pool
    struct pool_header *prevpool; // 在 usedpools 中指向上一个 pool
    uint arenaindex; // pool 所在 arena 在 arenas 数组中的索引
    uint szidx; // pool 中 block 大小的分级
    uint nextoffset; // 需要新的 block 可以从 nextoffset 处分配
    uint maxnextoffset; // nextoffset 最大有效值
};

typedef struct pool_header *poolp;

一旦被分配,每个池子都会处于三种状态之一:满、空或使用中。

  • full 所有的 block 都分配了

  • empty 所有的 block 都是空闲的,都可用于分配,所有处于 empty 状态的 pool 都在其所在 arena_object 的 freepools 字段表示的单链表中

  • used 有已分配的 block,也有空闲的 block,所有处于 used 状态的 pool 都在全局数组 usedpools 中某个元素指向的双向循环链表中

usedpools 是内存分配最常访问的数据结构,分配内存时先计算申请的内存大小对应的 block 分级 i,usedpools[i+i] 指向的就是属于分级 i 的所有处于 used 状态的 pool 构成的双向循环链表的头结点,如果链表不空就从头结点中选择一个空闲 block 分配。下面我们来探究一下为什么 usedpools[i+i] 指向的链表,属于分级 i 的池。

usedpools 的原始定义如下

#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),
    …
}

将宏定义稍微展开一下

static poolp usedpools[64] = { 
    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(6), PTA(6), PTA(7), PTA(7),
    …
}

PTA(x) 表示数组 usedpools 中第 2*x 个元素的地址减去两个指针的大小也就是 16 字节(64 位机器),假设数组 usedpools 首地址为 1000,则数组初始化的值如下图所示

Python 메모리 관리자는 풀링 기술을 어떻게 구현합니까?

假设 i = 2,则 usedpools[i+i] = usedpools[4] = 1016,数组元素的类型为 poolp 也就是 struct pool_header *,如果认为 1016 存储的是 struct pool_header,那么 usedpools[4] 和 usedpools[5] 的值也就是地址 1032 和 1040 存储的值,分别是字段 nextpool 和 prevpool 的值,可以得到

usedpools[4]->prevpool = usedpools[4]->nextpool = usedpools[4] = 1016

usedpools[4] 用指针 p 表示就有 p->prevpool = p->nextpool = p,那么 p 就是双向循环链表的哨兵节点,初始化的时候哨兵节点的前后指针都指向自己,表示当前链表为空。

虽然 usedpools 的定义非常绕,但是这样定义有个好处就是省去了哨兵节点的数据域,只保留前后指针,可以说是将节省内存做到了极致。

接下来我们将看一下 Python 3.10.4 源码中内存分配和释放逻辑的实现。另外说明一下,源码中有比本文详细的多注释说明,有兴趣的读者可以直接看源码,本文为了代码不至于过长会对代码做简化处理并且省略掉了大部分注释。

内存分配

内存分配的主逻辑在函数 pymalloc_alloc 中,简化后代码如下

static inline void*
pymalloc_alloc(void *ctx, size_t nbytes)
{  
    // 计算请求的内存大小 ntybes 所对应的内存分级 size
    uint size = (uint)(nbytes - 1) >> ALIGNMENT_SHIFT;
    // 找到属于内存分级 size 的 pool 所在的双向循环链表的头指针 pool
    poolp pool = usedpools[size + size];
    block *bp;
    // pool != pool->nextpool,说明 pool 不是哨兵节点,是真正的 pool
    if (LIKELY(pool != pool->nextpool)) {
        ++pool->ref.count;
        // 将 pool->freeblock 指向的 block 分配给 bp,因为 pool 是从 usedpools 中取的,
        // 根据 usedpools 的定义,pool->freeblock 指向的一定是空闲的 block
        bp = pool->freeblock;
        // 如果将 bp 分配之后 pool->freeblock 为空,需要从 pool 中划分一个空闲 block
        // 到 pool->freeblock 链表中留下次分配使用
        if (UNLIKELY((pool->freeblock = *(block **)bp) == NULL)) {
            pymalloc_pool_extend(pool, size);
        }
    }
    // 如果没有对应内存分级的可用 pool,就从 arena 中分配一个 pool 之后再从中分配 block
    else {
        bp = allocate_from_new_pool(size);
    }
    
    return (void *)bp;
}

主体逻辑还是比较清晰的,代码中注释都做了说明,不过还是要解释一下下面的这个判断语句。

if (UNLIKELY((pool->freeblock = *(block **)bp) == NULL))

前文已经介绍过 pool->freeblock 表示 pool 中可用来进行分配的 block 所在单链表的头指针,类型为 block*,但是 block 的定义为 typedef uint8_t block;,并不是一个结构体,所以没有指针域,那么是怎么实现单链表的呢。考虑到 pool->freeblock 的实际含义,只需要把空闲 block 用单链表串起来就可以了,不需要数据域,Python 内存管理器把空闲 block 内存的起始 8 字节(64 位机器)当做虚拟的 next 指针,指向下一个空闲 block,具体是通过 *(block **)bp 实现的。首先用 (block **) 将 bp 转换成 block 的二级指针,然后用 * 解引用,将 bp 指向内存的首地址内容转换成 (block *) 类型,表示下一个 block 的地址,不得不说,C 语言真的是可以为所欲为。再来看一下上面判断语句,首先将 bp 的下一个空闲 block 地址赋值给 pool->freeblock,如果是 NULL 证明没有更多空闲 block,需要调用 pymalloc_pool_extend 扩充。

pymalloc_pool_extend 的源码简化后如下

static void
pymalloc_pool_extend(poolp pool, uint size)
{
    // 如果 pool 还有更多空间,就划分一个空闲 block 放到 pool->freeblock 中
    if (UNLIKELY(pool->nextoffset <= pool->maxnextoffset)) {
        pool->freeblock = (block*)pool + pool->nextoffset;
        pool->nextoffset += INDEX2SIZE(size);
        // pool->freeblock 只有一个 block,需要将虚拟的 next 指针置为 NULL
        *(block **)(pool->freeblock) = NULL;
        return;
    }

    // 如果没有更多空间,需要将 pool 从 usedpools[size+size] 中移除
    poolp next;
    next = pool->nextpool;
    pool = pool->prevpool;
    next->prevpool = pool;
    pool->nextpool = next;

}

过程也很清晰,如果有更多空间就划分一个 block 到 pool->freeblock,如果没有更多空间就将 pool 从 usedpools[size+size] 中移除。pool->nextoffset 指向的是 pool 中从未被使用过内存的地址,分配 block 时候优先使用 pool->nextoffset 之前的空闲 block,这些空闲的 block 一般是之前分配过后来又被释放到 pool->freeblock 中的。这种复用空闲 block 的方式让 pool 更加经久耐用,如果每次都从 pool->nextoffset 划分一个新的 block,pool 很快就会被消耗完,变成 full 状态。

在 pymalloc_alloc 中如果没有可用 pool 就会调用 allocate_from_new_pool 先分配一个新的 pool,再从新的 pool 中分配 block,其源码简化后如下

static void*
allocate_from_new_pool(uint size)
{
    // 没有可用的 arena 就新申请一个
    if (UNLIKELY(usable_arenas == NULL)) {
        usable_arenas = new_arena();
        if (usable_arenas == NULL) {
            return NULL;
        }
        // 将新的 arena 作为 usable_arenas 链表的头结点
        usable_arenas->nextarena = usable_arenas->prevarena = NULL;
        nfp2lasta[usable_arenas->nfreepools] = usable_arenas;
    }

    // 如果有可用 arena 就从中分配一个空闲 pool,并调整当前 arena 在 usable_arenas 中的位置,使 usable_arenas 按空闲 pool 的数量从小到大排序
    if (nfp2lasta[usable_arenas->nfreepools] == usable_arenas) {
        nfp2lasta[usable_arenas->nfreepools] = NULL;
    }
    if (usable_arenas->nfreepools > 1) {
        nfp2lasta[usable_arenas->nfreepools - 1] = usable_arenas;
    }

    // 执行到这里,usable_arenas->freepools 就是当前需要的可用 pool
    poolp pool = usable_arenas->freepools;
    // 更新 freepools 链表和 nfreepools 计数
    if (LIKELY(pool != NULL)) {
        usable_arenas->freepools = pool->nextpool;
        usable_arenas->nfreepools--;
        // 分配之后,如果 arena 中没有空闲 pool,需要更新 usable_arenas 链表
        if (UNLIKELY(usable_arenas->nfreepools == 0)) {
            usable_arenas = usable_arenas->nextarena;
            if (usable_arenas != NULL) {
                usable_arenas->prevarena = NULL;
            }
        }
    }
    // 如果当前 arena 中没有可用 pool,就重新划分一个
    else {
        pool = (poolp)usable_arenas->pool_address;
        pool->arenaindex = (uint)(usable_arenas - arenas);
        pool->szidx = DUMMY_SIZE_IDX;
        usable_arenas->pool_address += POOL_SIZE;
        --usable_arenas->nfreepools;
        // 划分之后,如果 arena 中没有空闲 pool,需要更新 usable_arenas 链表
        if (usable_arenas->nfreepools == 0) {
            usable_arenas = usable_arenas->nextarena;
            if (usable_arenas != NULL) {
                usable_arenas->prevarena = NULL;
            }
        }
    }

    // 执行到这里,变量 pool 就是找到的可用 pool,将其置为链表 usedpools[size+size] 的头节点
    block *bp;
    poolp next = usedpools[size + size];
    pool->nextpool = next;
    pool->prevpool = next;
    next->nextpool = pool;
    next->prevpool = pool;
    pool->ref.count = 1;
    // 如果 pool 的内存分级跟请求的一致,直接从中分配一个 block 返回
    // 证明这个 pool 之前被使用之后又释放到 freepools 中了
    // 并且当时使用的时候内存分级也是 size
    if (pool->szidx == size) {
        bp = pool->freeblock;
        pool->freeblock = *(block **)bp;
        return bp;
    }
    
    // 执行到这里,说明 pool 是 arena 新划分的,需要对其进行初始化
    // 然后分配 block 返回
    pool->szidx = size;
    size = INDEX2SIZE(size);
    bp = (block *)pool + POOL_OVERHEAD;
    pool->nextoffset = POOL_OVERHEAD + (size << 1);
    pool->maxnextoffset = POOL_SIZE - size;
    pool->freeblock = bp + size;
    *(block **)(pool->freeblock) = NULL;
    return bp;
}

这段代码比较长,归纳一下做了下面 3 件事

  • 如果没有可用的 arena 就重新申请一个

  • 从可用的 arena 中分配一个新的 pool

  • 从分配的 pool 中分配空闲的 block

首先是 arena 的申请,申请流程在函数 new_arena() 中,申请完之后将对应的 arena_object 置为 双线链表 usable_arenas 的头结点,并且前后指针都置为 NULL,因为只有在没有可用 arena 的时候才回去调用 new_arena(),所以申请之后系统里只有一个可用 arena。另外还有一个操作如下

nfp2lasta[usable_arenas->nfreepools] = usable_arenas;

nfp2lasta 是一个数组,nfp2lasta[i] 表示的是在 usable_arenas 链表中,空闲 pool 的数量为 i 的所有 arena 中最后一个 arena。前文已经说明 usable_arenas 是按照 arena 中空闲 pool 的数量从小到大排序的,为了维护 usable_arenas 的有序性,在插入或删除一个 arena 的时候需要找到对应的位置,时间复杂度为 O(N),为了避免线性搜索,Python 3.8 引入了 nfp2lasta,将时间复杂度降为常量级别。

有了可用的 arena 就可以从中分配 pool 了,分配 pool 之后 arena->nfreepools 就会减少,需要更新 nfp2lasta,由于使用的是链表 usable_arenas 的头结点,并且是减少其空闲 pool 数量,所以整个链表依然有序。接下来优先复用 arena->freepools 中空闲的 pool,如果没有就从 arena->pool_address 指向的未使用内存处新划分一个 pool,这点跟 pool 中复用空闲 block 的策略是一样的。

分配了可用的 pool,先将其置为链表 usedpools[size+size] 的头结点,然后从中分配 block,如果 pool 不是从新分配的 arena 获得的,那么 pool 就是之前初始化使用之后释放掉的,如果 pool 的分级恰好就是请求的内存分级那么直接从 pool->freeblock 分配 block,否则需要将 pool 重新初始化,当然如果 pool 来自新分配的 arena 也要进行初始化。初始化的时候,先将第一个 block 的地址进行内存对齐,然后将 pool->freeblock 指向第 2 个 block 留下次分配使用(第 1 个 block 本次要返回),将 pool->nextoffset 指向第 3 个 block,在下次划分新的 block 时使用。

内存释放

内存释放的主逻辑在 pymalloc_free 函数中,代码简化后如下

static inline int
pymalloc_free(void *ctx, void *p)
{
    // 假设 p 是 pool 分配的,计算 p 所在 pool 的首地址
    poolp pool = POOL_ADDR(p);
    // 如果 p 不是内存管理器分配的直接返回
    if (UNLIKELY(!address_in_range(p, pool))) {
        return 0;
    }
    
    // 将 p 指向的 block 归还给 pool,置为 pool->freeblock 的头结点
    block *lastfree = pool->freeblock;
    *(block **)p = lastfree;
    pool->freeblock = (block *)p;
    pool->ref.count--;
    // 如果 pool 原来处于 full 状态,现在有一个空闲的 block 就变成了 used 状态
    // 需要将其作为头结点插到 usedpools[size+size] 中
    if (UNLIKELY(lastfree == NULL)) {
        insert_to_usedpool(pool);
        return 1;
    }

    if (LIKELY(pool->ref.count != 0)) {
        return 1;
    }

    // 如果 block 释放之后,其所在 pool 所有的 block 都是空闲状态,
    // 将 pool 从 usedpools[size+size] 中移到 arena->freepools 
    insert_to_freepool(pool);
    return 1;
}

pymalloc_free 函数的逻辑也很清晰

  • 计算地址 p 所在 pool 首地址,前文介绍过每个 pool 首地址都是 POOL_SIZE 的整数倍,所以将 p 的低位置 0 就得到了 pool 的地址

  • address_in_range(p, pool) 判断 p 是否是由 pool 分配的,如果不是直接返回

  • 将 p 指向的 block 释放掉,被 pool->freeblock 回收

  • 如果 pool 开始为 full 状态,那么回收 block 之后就是 used 状态,调用函数 insert_to_usedpool(pool) 将其置为 usedpools[size+size] 的头结点。这里的策略跟 usable_arenas 一样,优先使用快满的 pool,让比较空闲的 pool 有较高的概率被释放掉。

  • 如果 pool 回收 block 之后变成 empty 状态,需要调用 insert_to_freepool(pool) 将 pool 也释放掉

address_in_range 函数如下

address_in_range(void *p, poolp pool)
{
    uint arenaindex = *((volatile uint *)&pool->arenaindex);
    return arenaindex < maxarenas &&
        (uintptr_t)p - arenas[arenaindex].address < ARENA_SIZE &&
        arenas[arenaindex].address != 0;
}

这段逻辑能在常量时间内判断出 p 是否由 pool 分配,但是存在一个可能出问题的地方,毕竟这里的 pool 是在假设 p 是由 pool 分配的前提下计算出来的,有可能 pool 指向的地址可能还没被初始化,pool->arenaindex 操作可能会出错。Python 3.10 在这个 commit 中利用基数树来判断任意一个地址 p 是不是由内存管理器分配的,避免了可能出现的内存访问错误。

insert_to_usedpool 函数中只是简单的指针操作就不展开了,insert_to_freepool 稍微复杂一点,下面再展开一下

static void
insert_to_freepool(poolp pool)
{
    poolp next = pool->nextpool;
    poolp prev = pool->prevpool;
    next->prevpool = prev;
    prev->nextpool = next;
    // 将 pool 置为 ao->freepools 头结点
    struct arena_object *ao = &arenas[pool->arenaindex];
    pool->nextpool = ao->freepools;
    ao->freepools = pool;
    uint nf = ao->nfreepools;
    struct arena_object* lastnf = nfp2lasta[nf];
    // 如果 arena 是排在最后的包含 nf 个空闲 pool 的 arena,
    // 需要将 nfp2lasta[nf] 置为 arena 的前驱结点或 NULL
    if (lastnf == ao) { /* it is the rightmost */
        struct arena_object* p = ao->prevarena;
        nfp2lasta[nf] = (p != NULL && p->nfreepools == nf) ? p : NULL;
    }
    ao->nfreepools = ++nf;

    // 如果 pool 释放后 arena 变成完全空闲状态,并且系统中还有其他可用 arena,
    // 需要将 arena 从 usable_arenas 中移除并调用 free() 函数将其释放归还给操作系统
    if (nf == ao->ntotalpools && ao->nextarena != NULL) {
        if (ao->prevarena == NULL) {
            usable_arenas = ao->nextarena;
        }
        else {
            ao->prevarena->nextarena = ao->nextarena;
        }
        if (ao->nextarena != NULL) {
            ao->nextarena->prevarena = ao->prevarena;
        }
        ao->nextarena = unused_arena_objects;
        unused_arena_objects = ao;
        arena_map_mark_used(ao->address, 0);
        _PyObject_Arena.free(_PyObject_Arena.ctx, (void *)ao->address, ARENA_SIZE);
        ao->address = 0;          
        --narenas_currently_allocated;
        return;
    }
    // 如果 pool 释放后 arena 从满变成可用,需要将其置为 usable_arenas 头结点,
    // 因为 arena 空闲 pool 数量为 1,作为头结点不会破坏 usable_arenas 有序性
    if (nf == 1) {
        ao->nextarena = usable_arenas;
        ao->prevarena = NULL;
        if (usable_arenas)
            usable_arenas->prevarena = ao;
        usable_arenas = ao;
        if (nfp2lasta[1] == NULL) {
            nfp2lasta[1] = ao;
        }
        return;
    }

    if (nfp2lasta[nf] == NULL) {
        nfp2lasta[nf] = ao;
    } 
    // 如果 arena 本来就是包含 lastnf 个空闲 pool 的最后一个,现在空闲 pool 数量加 1,
    // 整个 usable_arenas 还是有序的
    if (ao == lastnf) {
        return;
    }

    // arena->nfreepools 的增加导致 usable_arenas 失序,
    // 重新调整 arena 在 usable_arenas 的位置
    if (ao->prevarena != NULL) {
        ao->prevarena->nextarena = ao->nextarena;
    }
    else {
        usable_arenas = ao->nextarena;
    }
    ao->nextarena->prevarena = ao->prevarena;
    ao->prevarena = lastnf;
    ao->nextarena = lastnf->nextarena;
    if (ao->nextarena != NULL) {
        ao->nextarena->prevarena = ao;
    }
    lastnf->nextarena = ao;
}

首先将这个空闲的 pool 置为 ao->freepools 的头结点,这样可以保证最后释放的 pool 会最先被使用,提高访存效率,因为之前释放的 pool 可能被置换出了物理内存。然后根据不同情况更新 nfp2lasta,便于后续维护 usable_arenas 的有序性。接着根据 pool 释放前后其所在 arena 状态的变化做不同操作。

  • 如果 arena 由可用状态变成空闲状态,并且系统中还有其他可用 arena,就调用 free() 将 arena 释放掉归还给操作系统。不释放唯一空闲 arena,以避免内存抖动。

  • 如果 arena 由不可用状态(所有 pool 都分配了)变成可用状态,将其置为 usable_arenas 的头结点。

  • 如果 pool 释放前后 arena 都是可用状态,也就是一直都在 usable_arenas 链表中,如果其可用 pool 数量的增加导致 usable_arenas 链表失序,需要移动 arena 到合适位置来保持 usable_arenas 的有序性。

위 내용은 Python 메모리 관리자는 풀링 기술을 어떻게 구현합니까?의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 yisu.com에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제