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