Python中的整数对象(《Python源码剖析》笔记二)

704 阅读9分钟

这是我的关于《Python源码剖析》一书的笔记的第二篇。Learn Python by Analyzing Python Source Code · GitBook

在《Python源码剖析》中,Python的版本为2.5,而在Python3中,前面提到,int类型的底层实现是Python2中的long类型。所以,我会在本章中,先介绍Python2源码中int类型的实现,再在最后介绍一下Python3.6中int(也就是以前的long)在底层的实现。之所以这样做的原因后面会解释。

在此之前,我们先来看一个有趣但没什么卵用的现象:

Python 3.6.1 (v3.6.1:69c0db5, Mar 21 2017, 17:54:52) [MSC v.1900 32 bit (Intel)] on win32
Type "copyright", "credits" or "license()" for more information.
>>> a=1
>>> b=1
>>> a is b
True
>>> a=256
>>> b=256
>>> a is b
True
>>> a=257
>>> b=257
>>> a is b
False

想知道为什么?看完这章就知道了。

初识PyIntObject对象

Python中对整数的概念的实现是通过PyIntObject来完成的。我们曾经说过它是一个定长对象,与之相对的还有变长对象。这样的分法对我们理解Python源码有帮助,但在Python语言的层面上,我们通常还使用一种二分法,即根据对象维护数据的可变性将对象分为可变对象(mutable)和不可变对象(immutable)。Python2中的PyIntObject是一个定长对象,而PyLongObject是一个变长对象,但它们都是不可变对象。也就是说,一旦创建了它们之后,就不能改变它们的值了。

我们来看一下在Python2.7中,PyIntObject的实现:

typedef struct {
    PyObject_HEAD
    long ob_ival;
} PyIntObject;

Python2中的整数对象PyIntObject实际上就是对C原生类型long的一个包装。

我们知道关于一个Python对象的大多数元信息是保存在它的类型对象中的,对于PyIntObject是PyInt_Type:

PyTypeObject PyInt_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "int",
    sizeof(PyIntObject),
    0,
    (destructor)int_dealloc,                    /* tp_dealloc */
    (printfunc)int_print,                       /* tp_print */
    0,                                          /* tp_getattr */
    0,                                          /* tp_setattr */
    (cmpfunc)int_compare,                       /* tp_compare */
    (reprfunc)int_to_decimal_string,            /* tp_repr */
    &int_as_number,                             /* tp_as_number */
    0,                                          /* tp_as_sequence */
    0,                                          /* tp_as_mapping */
    (hashfunc)int_hash,                         /* tp_hash */
    0,                                          /* tp_call */
    (reprfunc)int_to_decimal_string,            /* tp_str */
    PyObject_GenericGetAttr,                    /* tp_getattro */
    0,                                          /* tp_setattro */
    0,                                          /* tp_as_buffer */
    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_CHECKTYPES |
        Py_TPFLAGS_BASETYPE | Py_TPFLAGS_INT_SUBCLASS,          /* tp_flags */
    int_doc,                                    /* tp_doc */
    0,                                          /* tp_traverse */
    0,                                          /* tp_clear */
    0,                                          /* tp_richcompare */
    0,                                          /* tp_weaklistoffset */
    0,                                          /* tp_iter */
    0,                                          /* tp_iternext */
    int_methods,                                /* tp_methods */
    0,                                          /* tp_members */
    int_getset,                                 /* tp_getset */
    0,                                          /* tp_base */
    0,                                          /* tp_dict */
    0,                                          /* tp_descr_get */
    0,                                          /* tp_descr_set */
    0,                                          /* tp_dictoffset */
    0,                                          /* tp_init */
    0,                                          /* tp_alloc */
    int_new,                                    /* tp_new */
};

PyInt_Type中保存了关于PyIntObject的许多元信息,它定义了一个PyIntObject支持的操作,占用的内存大小等等。其中重要的内容我们会在下面叙述。

PyIntObject的创建和维护

创建对象的途径

在intobject.h中,Python提供了几种创建PyIntObject的途径:

PyAPI_FUNC(PyObject *) PyInt_FromString(char*, char**, int);
PyAPI_FUNC(PyObject *) PyInt_FromUnicode(Py_UNICODE*, Py_ssize_t, int);
PyAPI_FUNC(PyObject *) PyInt_FromLong(long);

事实上,前两种方法都是先将字符串转换为浮点数,再调用PyInt_FromFloat来实现的。

小整数对象

我们来思考一下,在Python内部,整数对象是如此广泛地被使用,尤其是那些比较小的整数。短短几秒之间,我们可能就要用的它们成千上万次,如果我们在创建它们的时候使用malloc来请求分配内存,删除它们时再调用free来释放内存。显然,这样的性能水平是不可能达到我们的要求的,而且也会造成极大的不必要的浪费。

于是,在Python内部,对于小整数使用了对象池技术。

[intobject.c]
#ifndef NSMALLPOSINTS
#define NSMALLPOSINTS           257
#endif
#ifndef NSMALLNEGINTS
#define NSMALLNEGINTS           5
#endif
#if NSMALLNEGINTS + NSMALLPOSINTS > 0
/* References to small integers are saved in this array so that they
   can be shared.
   The integers that are saved are those in the range
   -NSMALLNEGINTS (inclusive) to NSMALLPOSINTS (not inclusive).
*/
static PyIntObject *small_ints[NSMALLNEGINTS + NSMALLPOSINTS];
#endif

我们可以看出,在Python2.7中,“小整数”的定义是[-5,256],而这个指向一个整数对象数组的指针small_ints就是这个对象池机制的核心。如果我们想要重新定义“小整数”怎么办?简单,我们可以修改源代码并重新编译。

对于小整数对象,Python直接把它们缓存在小整数对象池中,用于共享。那么大整数呢?肯定不可能都缓存在内存中,但是说不定某些大整数在某个时刻会变得十分常用,不过谁也不知道究竟是哪个数字。这时候,Python选择了另一种策略。

大整数对象

Python的设计者的策略是:对于小整数对象,直接把它们全部缓存在对象池中。对于其他整数,Python运行环境将会提供一块内存空间,这块内存空间由这些对象轮流使用。

在Python中,有一个PyIntBlock结构,它被用来实现这个机制。

#define BLOCK_SIZE      1000    /* 1K less typical malloc overhead */
#define BHEAD_SIZE      8       /* Enough for a 64-bit pointer */
#define N_INTOBJECTS    ((BLOCK_SIZE - BHEAD_SIZE) / sizeof(PyIntObject))

struct _intblock {
    struct _intblock *next;
    PyIntObject objects[N_INTOBJECTS];
};

typedef struct _intblock PyIntBlock;

static PyIntBlock *block_list = NULL;
static PyIntObject *free_list = NULL;

PyIntBlock的单向列表通过block_list维护,每一个block中都维护了一个PyIntObject数组——objects,这就是真正用于存储被缓存的PyIntObject对象的内存。我们可以想象,在运行的某个时刻,这块内存中一定有一部分被使用,而有一部分是空闲的。这些空闲状态的内存需要被组织起来,以供Python在需要存储新的整数对象时使用。Python使用一个单向链表来管理全部block的objects中的空闲内存,这个链表的表头就是free_list。在一开始,block_list和free_list都指向NULL。

添加和删除

我们来看一下PyIntObject是如何从无到有地产生,又是如何消失的。

[intobject.c]
PyObject *
PyInt_FromLong(long ival)
{
    register PyIntObject *v;
#if NSMALLNEGINTS + NSMALLPOSINTS > 0 /* 尝试使用小整数对象池 */
    if (-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS) {
        v = small_ints[ival + NSMALLNEGINTS];
        Py_INCREF(v);
#ifdef COUNT_ALLOCS
        if (ival >= 0)
            quick_int_allocs++;
        else
            quick_neg_int_allocs++;
#endif
        return (PyObject *) v;
    }
#endif
    if (free_list == NULL) {
        if ((free_list = fill_free_list()) == NULL)
            return NULL;
    }
    /* Inline PyObject_New */
    v = free_list;
    free_list = (PyIntObject *)Py_TYPE(v);
    (void)PyObject_INIT(v, &PyInt_Type);
    v->ob_ival = ival;
    return (PyObject *) v;
}

使用小整数对象池

如果NSMALLNEGINTS + NSMALLPOSINTS > 0成立,说明小整数对象池机制被激活了,然后Python会检查传入的long值是否是小整数。如果是,就直接返回对象池中的小整数对象就可以了。如果不是,那么会转向通用整数对象池。Python会在block的objects中寻找一块可用于存储新的PyIntObject的内存,这个任务需要free_list来完成。

创建通用整数对象池

显然,当首次调用PyInt_FromLong时,free_list必定为NULL,这时Python会调用fill_free_list创建新的block。在Python运行期间,只要所有block的空闲内存被使用完了,也就是free_list指向NULL,那么下一次调用PyInt_FromLong时就会再次激活对fill_free_list的调用。

static PyIntObject *
fill_free_list(void)
{
    PyIntObject *p, *q;
    /* 申请大小为sizeof(PyIntBlock)的内存空间,并链接到已有的block list中 */
    p = (PyIntObject *) PyMem_MALLOC(sizeof(PyIntBlock));
    if (p == NULL)
        return (PyIntObject *) PyErr_NoMemory();
    ((PyIntBlock *)p)->next = block_list;
    block_list = (PyIntBlock *)p;
    /* 将PyIntBlock中的PyIntObject数组——objects转变成单向链表*/
    p = &((PyIntBlock *)p)->objects[0];
    q = p + N_INTOBJECTS;
    while (--q > p)
        Py_TYPE(q) = (struct _typeobject *)(q-1);
    Py_TYPE(q) = NULL;
    return p + N_INTOBJECTS - 1;
}

在这个函数中,会首先申请一个新的PyIntBlock结构,这时block中的objects还只是一个PyIntObject对象数组。接下来,Python将objects中的所有PyIntObject对象通过指针依次连接起来从而将数组转变成一个单向链表。z在整个链接过程中,Python使用PyObject的ob_type指针作为连接指针。当链表转换完成后,free_list也就出现在出现在它该出现的位置了。从free_list开始,沿着ob_type指针,就可以遍历刚刚创建的所有为PyIntObject准备的内存了。

说完了PyIntObject的创建,我们再来看看它的删除。在Python中,当一个对象引用计数变为0 时,Python就会着手将这个对象销毁。不同类型的对象在销毁时执行的动作也是不同的,其在与对象对应的类型对象中被定义——也就是tp_dealloc。

[intobject.c]
static void
int_dealloc(PyIntObject *v)
{
    if (PyInt_CheckExact(v)) {
        Py_TYPE(v) = (struct _typeobject *)free_list;
        free_list = v;
    }
    else
        Py_TYPE(v)->tp_free((PyObject *)v);
}

首先,Python会检查传入的对象是否真的是一个PyIntObject对象,如果是的话,那么将其链入free_list所维护的自由内存链表中,以供将来别的PyIntObject使用。如果只是整数对象的派生类型,那么简单地调用派生类型中指定的tp_free。

使用通用整数对象池

在Python运行的过程中,会不只有一个PyIntBlock存在于同一个链表中,但是它们维护的objects却是分离的,之间没有联系。我们设想一下,有两个PyIntBlock对象,PyIntBlock1和PyIntBlock2,前者已经被填满,后者还有空闲的空间。所以此时free_list指向的是PyIntBlock2.objects中的空闲的内存块。当前者维护的objects中有PyIntObject被删除了,这时前者出现了一块空闲的内存。那么下次创建新的PyIntObject对象时应该使用这块空闲内存。否则就意味着所有的内存只能使用一次,这和内存泄漏没什么两样。

怎么才能将空闲的内存再交由Python使用呢?关键就在于前面我们分析的PyIntObject的删除操作,通过int_dealloc中的操作,所有的PyIntBlock的objects中的空闲内存块都被链接在一起了。它们形成了一个单向链表,表头正是free_list。

小整数对象池的初始化

现在关于Python的整数对象机制还剩最后一个问题。小整数对象池是在什么时候被初始化的呢?

[intobject.c]
int _PyInt_Init(void)
{
    PyIntObject *v;
    int ival;
#if NSMALLNEGINTS + NSMALLPOSINTS > 0
    for (ival = -NSMALLNEGINTS; ival < NSMALLPOSINTS; ival++) {
        if (!free_list && (free_list = fill_free_list()) == NULL)
            return 0;
        /* PyObject_New is inlined */
        v = free_list;
        free_list = (PyIntObject *)Py_TYPE(v);
        (void)PyObject_INIT(v, &PyInt_Type);
        v->ob_ival = ival;
        small_ints[ival + NSMALLNEGINTS] = v;
    }
#endif
    return 1;
}

我们可以看到,通过_PyInt_Init的调用,Python创建了这些小整数对象,然后它们就会在整个运行周期中一直存在,直至解释器毁灭。

Python3中int的实现

int即long

我们在之前提到,在Python3中int底层实现就是以前Python2中的long类型。空口无凭,我们来看代码:

PyTypeObject PyLong_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "int",                                      /* tp_name */
    offsetof(PyLongObject, ob_digit),           /* tp_basicsize */
    sizeof(digit),                              /* tp_itemsize */
    long_dealloc,                               /* tp_dealloc */
    ……
    long_new,                                   /* tp_new */
    PyObject_Del,                               /* tp_free */
};

我们可以看到,PyLong_Type类型对象的tp_name就是int,也就是说,在Python内部,它就是int类型。

之所以我们在一开始不介绍Python3中的整数实现,是因为在Python3中没有了通用的整数对象池(至少我没有找到),不过还保留着小整数对象池。同时对于那些比较小也就是对于之前Python2中的long类型大材小用的整数来说,也有一个更加快速且节省资源的创建方式。

而且我还发现了一个彩蛋,就是在longobject.c中第二行的注释:

/* XXX The functional organization of this file is terrible */

哈哈哈,官方吐槽,最为致命。而且这个注释在Python2.7版本中也有,看来是一段陈年往事。