长亭百川云 - 文章详情

对象被销毁之后所占的内存一定会释放吗?解密浮点数的缓存池机制

古明地觉的编程教室

37

2024-07-13

楔子

浮点数这种对象经常容易被创建和销毁,因为它很简单,使用频率高。如果每次创建都借助操作系统分配内存、每次销毁都借助操作系统回收内存的话,那效率会低到什么程度,可想而知。

因此 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 结构体负责实现。

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2