python虚拟机集锦(3)-垃圾收集算法(3)

优化:重用字段以节省内存

为了节省内存,每个具有GC支持的对象中的两个链接列表指针可用于多种用途。这是一种常见的优化,称为“胖指针”或“标记指针”:携带额外数据的指针,“折叠”到指针中,意味着内联存储在表示地址的数据中,利用内存寻址的某些属性。这是可能的,因为大多数架构将某些类型的数据与数据的大小对齐,通常是一个字或多个字。这种差异使得指针的一些最低有效位未被使用,这些位可以用于标记或保存其他信息——最常见的是作为位字段(每个位都是一个单独的标记)——只要使用指针的代码在访问内存之前屏蔽掉这些位。E、 例如,在32位体系结构(对于地址和字大小)上,一个字是32位=4字节,因此字对齐的地址总是4的倍数,因此以00结尾,剩下最后2位可用;而在64位体系结构中,一个字是64位=8字节,所以字对齐的地址以000结尾,剩下最后3位可用。

CPython GC使用了两个胖指针,它们对应于内存布局和对象结构部分中讨论的PyGC_Head的额外字段:

由于存在额外信息,“标记tagged” 或“胖“tagged"指针不能直接取消引用,在获得实际内存地址之前,必须去除额外信息。需要特别注意直接操作链接列表的函数,因为这些函数通常假设列表中的指针处于一致状态。

  • _gc_prev字段通常用作“上一个”指针以维护双链接列表,但其最低两位用于保持标志prev_MASK_COLLECTING和_PyGC_REV_MASK_FINALIZED。在集合之间,唯一可以出现的标志是_PyGC_PREV_MASK_FINALIZED,它指示对象是否已完成。在集合期间,除了两个标志外,_gc_prev还临时用于存储引用计数(gc_ref)的副本,并且gc链接列表将变为单链接列表,直到恢复_gc_pref。
  • _gc_next字段用作“下一个”指针以维护双链接列表,但在收集期间,其最低位用于保持next_MASK_UNREACHABLE标志,该标志指示在循环检测算法期间对象是否暂时无法访问。这是只使用双链接列表来实现分区的一个缺点:虽然最需要的操作是恒定时间,但没有有效的方法来确定对象当前所在的分区。相反,当需要时,会使用特殊的技巧(如NEXT_MASK_UNREACHABLE标志)。

垃圾收集器C API

Python对检测和收集涉及循环引用的垃圾的支持需要对象类型的支持,这些对象类型是其他对象的“容器”,也可能是容器。不存储对其他对象的引用或只存储对原子类型(如数字或字符串)的引用的类型不需要为垃圾收集提供任何显式支持。

要创建容器类型,类型对象的tp_flags字段必须包含Py_TPFLAGS_HAVE_GC,并提供tp_traverse处理程序的实现。如果该类型的实例是可变的,则还必须提供tp_clear实现。

Py_TPFLAGS_HAVE_GC

具有此标志集的类型的对象必须符合此处记录的规则。为了方便起见,这些对象将被称为容器对象。

容器类型的构造函数必须符合两个规则:

必须使用PyObject_GC_New()或PyObject_GC_NewVar()分配对象的内存。

初始化可能包含对其他容器的引用的所有字段后,它必须调用PyObject_GC_Track()。

TYPEPyObject_GC_New(TYPE,PyTypeObject类型)

类似于PyObject_New(),但适用于设置了Py_TPFLAGS_HAVE_GC标志的容器对象。

TYPEPyObject_GC_NewVar(类型,PyTypeObject类型,Py_size_t大小)

类似于PyObject_NewVar(),但适用于设置了Py_TPFLAGS_HAVE_GC标志的容器对象。

TYPEPyObject_GC_Resize(TYPE,PyVarObjectop,Py_size_t newsize)

调整PyObject_NewVar()分配的对象的大小。失败时返回调整大小的对象或NULL。收集器还不能跟踪op。

void PyObject_GC_Track(PyObject*op)

将对象op添加到收集器跟踪的容器对象集。收集器可以在意外的时间运行,因此在跟踪对象时对象必须有效。一旦tp_traverse处理程序后面的所有字段都有效,通常在构造函数的末尾附近,就应该调用该函数。

同样,对象的解除定位器必须符合一对类似的规则:

在引用其他容器的字段无效之前,必须调用PyObject_GC_UnTrack()。

必须使用PyObject_GC_Del()释放对象的内存。

void PyObject_GC_Del(void*op)

使用PyObject_GC_New()或PyObject_GC_NewVar()释放分配给对象的内存。

void PyObject_GC_UnTrack(void*op)

从收集器跟踪的容器对象集中删除对象op。请注意,可以在此对象上再次调用PyObject_GC_Track(),将其添加回跟踪对象集。在tp_traverse处理程序使用的任何字段变为无效之前,deallocator(tp_dealloc处理程序)应该为对象调用此函数。

3.8版中的更改:_PyObject_GC_TRACK()和_PyObject_GC_UNTRACK()宏已从公共C API中删除。

tp_traverse处理程序接受以下类型的函数参数:

int*visitproc)(PyObject*对象,void*arg)

传递给tp_traverse处理程序的访问者函数的类型。调用该函数时,应将要遍历的对象作为对象,将tp_traverse处理程序的第三个参数作为arg。Python内核使用几个访问者函数来实现循环垃圾检测;预计用户不需要编写自己的访问者函数。

tp_traverse处理程序必须具有以下类型:

int*transverseproc)(PyObject*self,visitproc访问,void*arg)

容器对象的遍历函数。实现必须为self直接包含的每个对象调用访问函数,要访问的参数是包含的对象和传递给处理程序的arg值。不能使用NULL对象参数调用访问函数。如果访问返回非零值,则应立即返回该值。

为了简化tp_traverse处理程序的编写,提供了Py_VISIT()宏。为了使用此宏,tp_traverse实现必须将其参数精确命名为visit和arg:

void Py_VISIT(PyObject*o)

如果o不为NULL,则使用参数o和arg调用访问回调。如果visit返回非零值,则返回它。使用此宏,tp_traverse处理程序如下所示:

static int
my_traverse(Noddy *self, visitproc visit, void *arg)
{
    
    
    Py_VISIT(self->foo);
    Py_VISIT(self->bar);
    return 0;
}

tp_clear处理程序必须为查询类型,如果对象不可变,则为NULL。

int*inquiry)(PyObject*self)

删除可能已创建引用循环的引用。不可变对象不必定义此方法,因为它们永远不能直接创建引用循环。请注意,调用此方法后,对象必须仍然有效(不要只对引用调用Py_DECREF())。如果收集器检测到引用循环中涉及此对象,它将调用此方法。

优化:延迟跟踪容器

某些类型的容器不能参与引用循环,因此不需要由垃圾收集器跟踪。取消跟踪这些对象可以降低垃圾收集的成本。然而,确定哪些对象可以不被跟踪并不是免费的,必须权衡成本和垃圾收集的好处。有两种可能的策略可用于何时取消集装箱的跟踪:

  • 创建容器时。

    垃圾收集器检查容器时。

作为一般规则,原子类型的实例不被跟踪,而非原子类型(容器、用户定义的对象…)的实例被跟踪。但是,为了抑制简单实例的垃圾收集器占用空间,可以进行一些特定于类型的优化。从延迟跟踪中受益的本机类型的一些示例:

  • 只包含不可变对象(整数、字符串等,以及递归地不可变对象的元组)的元组不需要被跟踪。解释器创建了大量的元组,其中许多元组在垃圾收集之前将无法生存。因此,在创建时取消跟踪符合条件的元组是不值得的。相反,创建时会跟踪除空元组之外的所有元组。在垃圾收集期间,确定是否可以取消跟踪任何幸存的元组。如果尚未跟踪元组的所有内容,则可以取消跟踪元组。在所有垃圾收集周期中检查元组是否未跟踪。取消跟踪元组可能需要一个以上的周期。
  • 只包含不可变对象的字典也不需要跟踪。词典在创建时未被跟踪。如果被跟踪的项目被插入字典(作为关键字或值),则字典将被跟踪。在完全垃圾收集(所有代)期间,收集器将取消跟踪其内容未被跟踪的任何字典。

垃圾收集器模块提供Python函数is_track(obj),它返回对象的当前跟踪状态。后续的垃圾收集可能会更改对象的跟踪状态。

gc.is_tracked(0)
False
gc.is_tracked("a")
False
gc.is_tracked([])
True
gc.is_tracked({
    
    })
False
gc.is_tracked({
    
    "a": 1})
False
gc.is_tracked({
    
    "a": []})
True

猜你喜欢

转载自blog.csdn.net/AI_LX/article/details/128737383