使用类语法创建的CPython中的每个类实例都涉及循环垃圾收集机制。 这会增加每个实例的内存占用量,并可能在负载较重的系统中引起内存问题 。
如有必要,是否可以放弃一种基本的链接计数机制 ?
让我们分析一种有助于创建类的方法,该类的实例仅使用链接计数机制才能删除。
关于CPython中的垃圾收集的一些知识
Python中垃圾回收的主要机制是链接计数机制。 每个对象都包含一个字段,该字段包含指向它的链接数的当前值。 只要参考计数器的值变为零,便会销毁对象。 但是,它不允许处理包含循环引用的对象。 举个例子
lst = [] lst.append(lst) del lst
在这种情况下,删除对象后,对该对象的引用计数器仍大于零。 为了解决这个问题,Python提供了一种附加的机制来跟踪对象并中断对象之间链接图中的循环。 有一篇很好的文章介绍了CPython3中的垃圾回收机制如何工作。
与垃圾收集机制相关的开销
通常,垃圾回收机制不会引起问题。 但是有一些相关的开销:
分配每个内存类后,将添加PyGC_Head标头:(在64位平台上,Python <= 3.7中至少为24个字节,在3.8中至少为16个字节。
如果您运行同一进程的许多实例,这可能会导致内存不足的问题,在该实例中,您需要具有数量相对较少的属性的大量对象,并且内存大小受到限制。
有时是否有可能将自己局限于链接计数的基本机制?
当类表示非递归数据类型时,循环垃圾收集的机制可能是多余的。 例如,包含简单类型值(数字,字符串,日期/时间)的记录。 为了说明,考虑一个简单的类:
class Point: x: int y: int
如果正确使用,则不可能进行链接循环。 尽管在Python中,没有什么可以阻止“踢自己的脚”:
p = Point(0, 0) px = p
但是,对于Point
类,可以将自己限制为一种链接计数机制。 但是,还没有用于拒绝单个类的循环垃圾收集的标准机制。
现代CPython的设计使其在定义负责定义自定义类的类型的结构中定义自定义类时,始终设置Py_TPFLAGS_HAVE_GC标志。 它确定类实例将包含在垃圾回收机制中。 对于所有此类对象,在创建时都会添加PyGC_Head标头,并将它们包含在受监视对象的列表中。 如果未设置Py_TPFLAGS_HAVE_GC
标志,则仅基本链接计数机制起作用。 但是,一次重置Py_TPFLAGS_HAVE_GC
将不起作用。 您将需要对负责创建和销毁实例的核心CPython进行更改。 这仍然是有问题的。
关于一种实施
作为实现此想法的示例,请考虑recordclass项目中的基类dataobject
。 使用它,您可以创建其实例不参与循环垃圾收集机制的类(未安装Py_TPFLAGS_HAVE_GC
,因此没有其他PyGC_Head
标头)。 它们在内存中的结构与带有__slots__的类实例的结构完全相同,但没有PyGC_Head
:
from recordclass import dataobject class Point(dataobject): x:int y:int >>> p = Point(1,2) >>> print(p.__sizeof__(), sys.getsizeof(p)) 32 32
为了进行比较,我们给一个类似的类__slots__
:
class Point: __slots__ = 'x', 'y' x:int y:int >>> p = Point(1,2) >>> print(p.__sizeof__(), sys.getsizeof(p)) 32 64
大小差异恰好是PyGC_Head
标头的大小。 对于具有多个属性的实例,RAM中其迹线大小的这种增加可能很重要。 对于Point
类的实例,添加PyGC_Head
使其大小增加2倍。
为了实现此效果,使用了特殊的元类datatype
,该datatype
提供了dataobject
子类。 作为配置的结果, Py_TPFLAGS_HAVE_GC
标志被Py_TPFLAGS_HAVE_GC
, tp_basicsize实例的基本大小增加了存储其他字段槽所需的数量。 声明该类时,将列出相应的字段名称( Point
类具有两个: x
和y
)。 metatlass datatype
还提供设置插槽tp_alloc , tp_new , tp_dealloc , tp_free的值 ,这些值实现用于在内存中创建和销毁实例的正确算法。 默认情况下,实例缺少__weakref__和__dict__ (类似于带有__slots__
类实例)。
结论
可以看到,在CPython中,如果确信其实例不会形成循环链接,则有可能在特定类中禁用循环垃圾收集机制。 这将通过PyGC_Head
标头的大小减少它们在内存中的PyGC_Head
。