09月17, 2020

28. 创建 GC 管理的对象

我们已经直到 Integer、String 等简单 Object 可以完全通过引用计数完成内存管理,List、Tuple、Dict 等对象被成为容器(container),其中又可能包含其他容器而形成 “循环引用”,使得引用计数方案失效。GC 是引用技术方案的补充。从之前研究过的 List 对象中,可以看到其与 GC 打交道的影子:

// Objects/listobject.c:179

// 先在 GC 中创建 ListObject
op = PyObject_GC_New(PyListObject, &PyList_Type);

// Objects/listobject.c:197
// 让 GC 开始跟踪该对象
_PyObject_GC_TRACK(op);

对比之前的 LongObject 的创建:

// Objects/longobject.c:275

// 直接在内存池中申请对象
result = PyObject_MALLOC(offsetof(PyLongObject, ob_digit) +
                             size*sizeof(digit));

显然,在 GC 中创建的对象与直接在内存中创建有一些区别,否则 ListObject 的创建就没必要多此一举使用 PyObject_GC_New 接口了。如果一直深入 PyObject_GC_New 函数,最后就可以看到这样的代码:

// Modules/gcmodule.c:1952
_PyObject_GC_Alloc(int use_calloc, size_t basicsize)
{
    // 忽略一些无关代码

    // 实际申请的内存中额外申请了一块 PyGC_Head
    size = sizeof(PyGC_Head) + basicsize;

    // 申请内存
    if (use_calloc)
        g = (PyGC_Head *)PyObject_Calloc(1, size);
    else
        g = (PyGC_Head *)PyObject_Malloc(size);

    // 其他代码
}

注意,Python 3.8 中此处代码与 2.5 版本有一些差异。

可以看到 PyObject_GC_New 最终调用的其实还是 PyObject_* 接口,区别在于,GC 管理下的内存增加了一些附加信息 PyGC_Head,从名称也可以清除的看出来这是一段仅与 GC 相关的信息,其结构:

// Include/cpython/objimpl.h:45

typedef struct {
    uintptr_t _gc_next;
    uintptr_t _gc_prev;
} PyGC_Head;

倒是非常简单,显然是一个双向链表节点的前、后指针,大致可以猜的到,GC 通过双向链表管理这些 containers。

同样的,Python 3.8 对 PyGC_Head 做了一些优化,一些原有的信息被复用到这两个指针中,后面我们会看到是如何利用这两个指针复用表示 ref_cnt、obj unreachable、collecting 等更多信息。由于 Object 是 Python 中非常普遍的概念,这种内存优化实际上降低了代码的可读性,但是在大量对象时,对内存使用率的提升是可观的。

综合目前已知的信息,我们可以确定,这些在 GC 管理下的对象结构应该看起来这样,希望你还记得 PyObject 的结构,如果忘记了可以翻看前文:

image.png

明日继续。

本文链接:http://www.thinkinpython.com/post/deep_python_vm_28.html

-- EOF --