Python中的整數物件(《Python原始碼剖析》筆記二)

鬆直發表於2017-07-03

這是我的關於《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版本中也有,看來是一段陳年往事。


相關文章