楔子
浮点数这种对象经常容易被创建和销毁,因为它很简单,使用频率高。如果每次创建都借助操作系统分配内存、每次销毁都借助操作系统回收内存的话,那效率会低到什么程度,可想而知。
因此 Python 解释器在操作系统之上封装了一个内存池,在内存管理的时候会详细介绍,目前可以认为内存池就是解释器预先向操作系统申请的一部分内存,专门用于小对象的快速创建和销毁,从而避免了频繁和操作系统打交道,这便是 Python 的内存池机制。
但浮点数的使用频率很高,并且使用时还会创建和销毁大量的临时对象,举个例子:
a = 95.5
b = 117.3
c = 108.9
avg = (a + b + c) / 3
计算平均值的时候,会先计算 a + b,创建一个临时对象。接着让临时对象和 c 相加再创建一个临时对象,然后除以 3 得到结果。最后销毁临时对象,并将结果交给变量 avg。
尽管我们平常很少注意到这些,但运算背后所产生的对象的创建和销毁的次数,比我们想象的要多。特别是在循环的时候,会伴随大量的对象创建和销毁操作。
如果每次创建和销毁对象都要伴随着内存操作,这个时候即便有内存池机制,效率也是不高的,因为使用内存池虽然可以不经过操作系统,但它也会增加解释器系统的开销。
因此解释器在浮点数对象被销毁后,并不急着回收对象所占用的内存,换句话说其实对象还在,只是将该对象放入一个空闲的链表中。
之前我们说对象可以理解为一片内存空间,对象如果被销毁,那么理论上内存空间要归还给操作系统,或者回到内存池中。但 Python 考虑到效率,并没有真正地释放内存,而是将对象放入到链表中,占用的内存还在。
后续如果需要创建新的浮点数对象时,那么从链表中直接取出之前放入的对象(我们认为被回收的对象),然后根据新的浮点数对象重新初始化对应的字段即可,这样就避免了内存分配造成的开销。而这个链表就是我们说的缓存池,当然不光浮点数对象有缓存池,Python 的很多其它对象也有对应的缓存池,比如列表。
缓存池的实现细节
下面看一下浮点数的缓存池的具体细节。
// Objects/floatobject.c
// 浮点数的缓存池(链表)长度为 100
// 因此池子里面最多容纳 100 个 PyFloatObject
#define PyFloat_MAXFREELIST 100
// Include/internal/pycore_floatobject.h
struct _Py_float_state {
// 缓存池已经容纳了多少个 PyFloatObject
int numfree;
// 指向缓存池(链表)的头节点
PyFloatObject *free_list;
};
补充一下:在之前的 Python 源码中,比如 3.8 版本,缓存池是这么定义的。
#define PyFloat_MAXFREELIST 100
static int numfree = 0;
static PyFloatObject *free_list = NULL;
在 3.8 的时候,numfree 和 free_list 是以静态全局变量的形式出现的,而在 3.12 里面则是将它们组合成了一个结构体,但实现原理没太大变化。
不过问题来了,为什么要将它们组成一个结构体呢?下面解释一下原因。
首先解释器启动之后会创建一个主进程,这是毋庸置疑的,而主进程在底层也会对应一个对象,我们称之为进程状态对象,它由 PyInterpreterState 结构体负责实现。