您现在的位置是:网站首页> 编程资料编程资料
Python内存管理器如何实现池化技术_python_
2023-05-26
386人已围观
简介 Python内存管理器如何实现池化技术_python_
前言
Python 中一切皆对象,这些对象的内存都是在运行时动态地在堆中进行分配的,就连 Python 虚拟机使用的栈也是在堆上模拟的。既然一切皆对象,那么在 Python 程序运行过程中对象的创建和释放就很频繁了,而每次都用 malloc() 和 free() 去向操作系统申请内存或释放内存就会对性能造成影响,毕竟这些函数最终都要发生系统调用引起上下文的切换。下面我们就来看看 Python 中的内存管理器是如何高效管理内存的。
其实核心就是池化技术,一次性向操作系统申请一批连续的内存空间,每次需要创建对象的时候就在这批空间内找到空闲的内存块进行分配,对象释放的时候就将对应的内存块标记为空闲,这样就避免了每次都向操作系统申请和释放内存,只要程序中总的对象内存空间稳定,Python 向操作系统申请和释放内存的频率就会很低。这种方案是不是很熟悉,数据库连接池也是类似的思路。一般后端应用程序也是提前跟数据库建立多个连接,每次执行 SQL 的时候就从中找一个可用的连接与数据库进行交互,SQL 完成的时候就将连接交还给连接池,如果某个连接长时间未被使用,连接池就会将其释放掉。本质上,这些都是用空间换时间,消耗一些不算太大的内存,降低诸如内存申请和 TCP 建立连接等耗时操作的频率,提高程序整体的运行速度。
接下来具体看看 Python 的内存管理器是如何实现池化技术的,先概要介绍内存层次结构及分配内存的流程,然后结合源码详细展开。
内存层次结构
Python 内存管理器对内存进行了分层,从大到小分别为 arena、pool 和 block。arena 是内存管理器直接调用 malloc() 或 calloc() 向操作系统申请的一大块内存,Python 中对象的创建和释放都是在 arena 中进行分配和回收。在 arena 内部又分成了多个 pool,每个 pool 内又分成了多个大小相等的 block,每次分配内存的时候都是从某个 pool 中选择一块可用的 block 返回。每个 pool 内的 block 的大小是相等的,不同 pool 的 block 大小可以不等。

arena、pool 和 block 的大小在 32 位机器和 64 位机器上有所不同,block 的大小必须是 ALIGNMENT 的倍数,并且最大为 512 字节,下表列出了不同机器上各种内存的大小。
| 32 位机器 | 64 位机器 | |
|---|---|---|
| arena size | 256 KB | 1 MB |
| pool size | 4 KB | 16 KB |
| ALIGNMENT | 8 B | 16 B |
以 64 位机器为例,所有可能的 block 的大小为 16、32、48 … 496、512,每个大小都对应一个分级(size class),从小到大依次为0、1、2 … 30、31。每次分配内存的时候就是找到一个不小于请求大小的最小的空闲 block。对 block 的大小进行分级是为了适应不同大小的内存请求,减少内存碎片的产生,提高 arena 的利用率。
内存管理逻辑
了解了 arena、pool 和 block 的概念后就可以描述内存分配的逻辑了,假如需要的内存大小为 n 字节
- 如果 n > 512,回退为 malloc(),因为 block 最大为 512 字节
- 否则计算出不小于 n 的最小的 block size,比如 n=105,在 64 位机器上最小的 block size 为 112
- 找到对应 2 中 block size 的 pool,从中分配一个 block。如果没有可用的 pool 就从可用的 arena 中分配一个 pool,如果没有可用的 arena 就用 malloc() 向操作系统申请一块新的 arena
释放内存的逻辑如下
- 先判断要释放的内存是不是由 Python 内存管理器分配的,如果不是直接返回
- 找到要释放的内存对应的 block 和 pool,并将 block 归还给 pool,留给下次分配使用
- 如果释放的 block 所在的 arena 中除了自己之外其他的都是空闲的,那么在 block 归还之后整个 arena 都是空闲的,就可以将 arena 用 free() 释放掉还给操作系统
Python 中的对象一般都不大,并且生命周期很短,所以 arena 一旦申请之后,对象的分配和释放大部分情况下都是在 arena 中进行的,提高了效率。
上文已经将 Python 内存管理器的核心逻辑描述清楚了,只不过有一些细节的问题还没解决,比如内存分配的时候怎么根据 block size 找到对应的 pool,这些 pool 之间怎么关联起来的,内存释放的时候又是怎么判断要释放的内存是不是 Python 内存管理器分配的,等等。下面结合源码将内存分配和释放的逻辑详细展开。
先介绍 arena 和 pool 的内存布局和对应的数据结构,然后再具体分析 pymalloc_alloc() 和 pymalloc_free() 的逻辑,以 64 位机器为例介绍。
内存布局及对应的数据结构
Arena

arena 为 1 MB,pool 为 16 KB,pool 在 arena 中是相邻的,一个 arena 中最多有 1 MB / 16 KB = 64 个 pool。Python 内存管理器会将 arena 中第一个 pool 的首地址跟 POOL_SIZE 对齐,这样每个 pool 的首地址都是 POOL_SIZE 的整数倍,给定任意内存地址都可以很方便的计算出其所在 pool 的首地址,这个特性在内存释放的时候会用到。POOL_SIZE 在 32 位机器上是 4 KB,在 64 位机器上是 16 KB,这样做还有另外一个好处就是让每个 pool 正好落在一个或多个物理页中,提高了访存效率。上图中的灰色内存块就是为了对齐而丢弃掉的,如果 malloc() 分配的内存首地址恰好对齐了,那么 pool 的数量就是 64,否则就是 63。当然 arena 不是一开始就将全部的 pool 都划分出来,而是在没有可用的 pool 的时候才会去新划分一个,当所有的 pool 全部划分之后布局如上图所示。
每个 arena 都由结构体 struct arena_object 来表示,但不是所有 struct arena_object 都有对应的 arena,因为 arena 释放之后对应的 struct arena_object 还保留着,这些没有对应 arena 的 struct arena_object 存放在单链表 unused_arena_objects 中,在下次分配 arena 时可以拿来使用。如果 struct arena_object 有对应的 arena,并且 arena 中有可以分配的 pool,那么 struct arena_object 会存放在 usable_arenas 这个双向链表中,同时,所有的 struct arena_object 无论有没有对应的 arena 都存在数组 arenas 中。usable_arenas 中 arena 是按照其包含的空闲 pool 的数量从小到大排序的,这么排序是为了让已经使用了更多内存的 arena 在下次分配 pool 的时候优先被使用,那么在释放内存的时候排在后面的那些拥有更多空闲内存的 arena 就有更大可能变成完全空闲状态,从而被释放掉将其内存空间归还给操作系统,降低整体的内存消耗。
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

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; 每个 pool 一旦被分配之后一定会处于 full、empty 和 used 这 3 种状态中的一种。
- 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 的 pool 所在的链表。
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,则数组初始化的值如下图所示

假设 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 的源码简化后如下
本站声明:
1、本站所有资源均来源于互联网,不保证100%完整、不提供任何技术支持;
2、本站所发布的文章以及附件仅限用于学习和研究目的;不得将用于商业或者非法用途;否则由此产生的法律后果,本站概不负责!
相关内容
- 对Python中GIL(全局解释器锁)的一点理解浅析_python_
- Python标准库之日期、时间和日历模块_python_
- Python基础之模块详解_python_
- Python使用shutil操作文件、subprocess运行子程序_python_
- Python使用psutil获取系统信息_python_
- Python函数高级(命名空间、作用域、装饰器)_python_
- Python函数基础(定义函数、函数参数、匿名函数)_python_
- Python+Seaborn绘制分布图的示例详解_python_
- Python文件系统模块pathlib库_python_
- Python文件处理、os模块、glob模块_python_
点击排行
本栏推荐
