09月17, 2020

44. TupleObject 是怎样工作的?

TupleObject 是 Python 中另外一种常用对象,尤其是赋予了函数返回多个对象的能力。Tuple 可以看作是 “只读” 版本的 List,在最底层的 Object * 数组操作上二者有异曲同工之妙,加上 Tuple 的只读特性,Tuple 比 List 更加简单。今天仅通过一文快速掌握 Tuple 的大体工作原理。

TupleObject 的结构

按照惯例,第一步当然还是先认识 Tuple 到底长什么样子,Tuple 的结构非常简单且紧凑:

// Objects/tupleobject.h:9

typedef struct {
    PyObject_VAR_HEAD
    PyObject *ob_item[1];
} PyTupleObject;

还是熟悉的配方,熟悉的味道,PyObject_VAR_HEAD 打头,后面紧跟着一个 PyObject 数组。需要注意的是,虽然在结构体的定义中只有一个 ob_item,但实际在其后连续内存中,可能有更多 Object 。从 Tuple 的定义来看,它也是一个容器:

image.png

图中 PyTupleObject 所指示的区域是严格的 PyTupleObject 定义,但实际上 C 语言的数组没有边界检查,无限延长 ob_item 也是没问题的,图中所示的是一个包含 2 个 Object 的 TupleObject。这是一种 Python 中非常常用的技巧。

对比 ListObject 的结构,Tuple 的类型信息与数据区是连续的,套用 DictObject 中的说法,它们是 combined 模式。

Tuple 的创建

Python 中所有的容器类型,都需要在由 GC 管理,其他普通类型只需借助引用计数即可完成内存管理工作。Tuple 的数据区保存的是通用的 PyObject ,作为一个容器,它的创建当然也离不开 GC_New_ 之类的函数。

// Objects/tupelobject.c:80

PyObject *
PyTuple_New(Py_ssize_t size){
        // ··· ···

        // Objects/tupelobject.c:118
        op = PyObject_GC_NewVar(PyTupleObject, &PyTuple_Type, size);
        // ··· ···
}

// Include/objimpl.h:257
#define PyObject_GC_NewVar(type, typeobj, n) \
                ( (type *) _PyObject_GC_NewVar((typeobj), (n)) )

// Modules/gcmodule.c:2005
PyVarObject *
_PyObject_GC_NewVar(PyTypeObject *tp, Py_ssize_t nitems)
{
    size_t size;
    PyVarObject *op;

    // ··· ···

    // size = basic_size + nitems * item_size
    size = _PyObject_VAR_SIZE(tp, nitems);

    // GC Malloc here
    op = (PyVarObject *) _PyObject_GC_Malloc(size);

    // ··· ···
}

经过层层调用,终于还是经过 _PyObject_GC_Malloc 将内存分配在 GC 的控制之下,实际分配的内存大小是:

size = basic_size + nitems * item_size

即类型的基本尺寸 + 容纳数据尺寸 * 数量。其中 basic_size 和 item_size 在这里定义:

// Objects/tupleobject.c:829

PyTypeObject PyTuple_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "tuple",
    sizeof(PyTupleObject) - sizeof(PyObject *),   // tp_basicsize
    sizeof(PyObject *),                           // tp_itemsize
    //  ··· ···
};

其实就是图中所示的大小:

image.png

Tuple 采用连续存储是因为它是 “只读” 的,一旦创建好就不会再伸缩。实际上 Tuple 被创建以后一段时间内,当一切还在 C 控制之下时,Tuple 的数据可能是 NULL 或者被改变,但是一旦 Tuple 进入 Python 世界它就不可以在改变了。

Tuple 的 “只读” 是如何实现的呢?其实在 C 语言这一层,一切都可以被操作, Tuple 的只读只是在 Python 语言层面才有意义,毕竟 Tuple 被创建出来的时候还是一穷二白,初始化过程中的 set item 必定是存在的,但是我们无法直接通过 Python 代码修改 Tuple items。

对元素的操作

Tuple 被创建并初始化后,Tuple 就可以大致看作是一个普通的 C array 了。对 items 的读写实际上都是对 Tuple.ob_items[] 数组的操作,逻辑并不复杂。元素的长度则更直接,就是 PyTupleObject 的 ob_size 值。

Tuple 的缓存

Python 在性能优化上的努力可以说是不遗余力,几乎所有类型都提供了缓存 freelist,Tuple 也不例外。

// Objects/tupleobject.c:16

#ifndef PyTuple_MAXSAVESIZE
#define PyTuple_MAXSAVESIZE     20  /*缓存 Tuple 的 size 上限*/
#endif
#ifndef PyTuple_MAXFREELIST
#define PyTuple_MAXFREELIST  2000  /* 每一种 size Tuple 缓存最大数量*/
#endif

#if PyTuple_MAXSAVESIZE > 0

// 缓存数组,保存的是各尺寸 Tuple 链表头
static PyTupleObject *free_list[PyTuple_MAXSAVESIZE];
// 对应尺寸 Tuple 的链表长度
static int numfree[PyTuple_MAXSAVESIZE];
#endif

从定义可知,Tuple 的 freelist 只缓存 size 在 [0, 20] 之间的 Tuple,每一种尺寸最多缓存 2000 个实例。定义看起来不太清晰,看图其实就没多复杂:

image.png

图中索引表示的是 Tuple size,free_list[3] 缓存的是大小为 3 的 Tuple 构成链表的头部, numfree[3] 表示缓存中 size == 3 的 Tuple 数量。

在最开始的时候,freelist 是一个全为 NULL 的数组,没有缓存任何数据。当有 Tuple 被析构的时候,满足 size 在 [0,20],并且 free list 未满的情况下,即将被析构的 Tuple 就获得一线生机进入 freelist,避免了被垃圾回收的命运。

// Objects/tupleobject.c:237

static void
tupledealloc(PyTupleObject *op)
{
    //··· ···

        // size 满足条件,缓存未满
        if (len < PyTuple_MAXSAVESIZE &&
            numfree[len] < PyTuple_MAXFREELIST &&
            Py_TYPE(op) == &PyTuple_Type)
        {
            // 插入链表
            op->ob_item[0] = (PyObject *) free_list[len];
            numfree[len]++;
            free_list[len] = op;
            goto done; /* return */
        }
    // ··· ···
}

这里链表的组织特别有意思,并没有在 PyTupleObject 中新增形如 * next 的指针成员,而是直接复用了 Tuple->items[0] 作为链表节点:

image.png

图中将 freelist[20] 链表展开绘制,如果你熟悉链表的操作,结合上面的代码就很容易明白代码的意图。注意 numfree[20] 现在是 3 了,刚好是缓存 Tuple 实例的数量。

你是空,长度为 0 的空空

在 PyTuple_New 函数中有这样的代码:

// Objects/tupleobject.c:89

// size 为 0 的 Tuple 直接使用缓存,先看 125 行再看这里
if (size == 0 && free_list[0]) {
        op = free_list[0];

// Objects/tupleobject.c:125
    if (size == 0) {
        free_list[0] = op;
        ++numfree[0];
        Py_INCREF(op);          /* extra INCREF so that this is never freed */
    }

在第一次用到 size 为 0 的 Tuple 的时候,该实例被缓存在 free_list[0] 中,并且额外给它增加了一个引用,这样它的引用计数永远也不会减到 0, 可以获得 “永生了”。之后所有空 Tuple 实际上都引用的同一个实例 numfree[0]。

这个非常厉害的空 Tuple 会一直保留到 Python 主动释放它为止。释放过程在 PyTuple_Fini 函数中。

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

-- EOF --