Python 整數物件實現原理
整數物件在Python內部用PyIntObject
結構體表示:
typedef struct { PyObject_HEAD long ob_ival; } PyIntObject;
PyObject_HEAD巨集中定義的兩個屬性分別是:
int ob_refcnt; struct _typeobject *ob_type;
這兩個屬性是所有Python物件固有的:
- ob_refcnt:物件的引用計數,與Python的記憶體管理機制有關,它實現了基於引用計數的垃圾收集機制
- ob_type:用於描述Python物件的型別資訊。
由此看來PyIntObject就是一個對C語言中long型別的數值的擴充套件,出於效能考慮,對於小整數,Python使用小整數物件池small_ints
快取了[-5,257)之間的整數,該範圍內的整數在Python系統中是共享的。
#define NSMALLPOSINTS 257 #define NSMALLNEGINTS 5 static PyIntObject *small_ints[NSMALLNEGINTS + NSMALLPOSINTS];
而超過該範圍的整數即使值相同,但物件不一定是同一個,如下所示:當a與b的值都是10000,但並不是同一個物件,而值為1的時候,a和b屬於同一個物件。
>>> a = 10000 >>> b = 10000 >>> print a is b False >>> a = 1 >>> b = 1 >>> print a is b True
對於超出了[-5, 257)之間的其他整數,Python同樣提供了專門的緩衝池,供這些所謂的大整數使用,避免每次使用的時候都要不斷的malloc分配記憶體帶來的效率損耗。這塊記憶體空間就是PyIntBlock
。
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
,表頭始終指向最新建立的PyIntBlock物件。
PyIntBlock有兩個屬性:next,objects。next指標指向下一個PyIntBlock物件,objects是一個PyIntObject陣列(最終會轉變成單向連結串列),它是真正用於儲存被快取的PyIntObjet物件的記憶體空間。
free_list
單向連結串列是所有PyIntBlock記憶體塊中空閒的記憶體。所有空閒記憶體通過一個連結串列組織起來的好處就是在Python需要新的記憶體來儲存新的PyIntObject物件時,能夠通過free_list
快速獲得所需的記憶體。
建立一個整數物件時,如果它在小整數範圍內,就直接從小整數緩衝池中直接返回,如果不在該範圍內,就開闢一個大整數緩衝池記憶體空間:
[intobject.c] PyObject* PyInt_FromLong(long ival) { register PyIntObject *v; #if NSMALLNEGINTS + NSMALLPOSINTS > 0 //[1] :嘗試使用小整數物件池 if (-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS) { v = small_ints[ival + NSMALLNEGINTS]; Py_INCREF(v); return (PyObject *) v; } #endif //[2] :為通用整數物件池申請新的記憶體空間 if (free_list == NULL) { if ((free_list = fill_free_list()) == NULL) return NULL; } //[3] : (inline)內聯PyObject_New的行為 v = free_list; free_list = (PyIntObject *)v->ob_type; PyObject_INIT(v, &PyInt_Type); v->ob_ival = ival; return (PyObject *) v; }
fill_free_list
就是建立大整數緩衝池記憶體空間的邏輯,該函式返回一個free_list
連結串列,當整數物件ival建立成功後,free_list
表頭就指向了v->ob_type
,ob_type
不是所有Python物件中表示型別資訊的欄位嗎?怎麼在這裡作為一個連線指標呢?這是Python在效能與程式碼優雅之間取中庸之道,對名稱的濫用,放棄了對型別安全的堅持。把它理解成指向下一個PyIntObject的指標即可。
[intobject.c] static PyIntObject* fill_free_list(void) { PyIntObject *p, *q; // 申請大小為sizeof(PyIntBlock)的記憶體空間 // block list始終指向最新建立的PyIntBlock p = (PyIntObject *) PyMem_MALLOC(sizeof(PyIntBlock)); ((PyIntBlock *)p)->next = block_list; block_list = (PyIntBlock *)p; //:將PyIntBlock中的PyIntObject陣列(objects)轉變成單向連結串列 p = &((PyIntBlock *)p)->objects[0]; q = p + N_INTOBJECTS; while (--q > p) // ob_type指向下一個未被使用的PyIntObject。 q->ob_type = (struct _typeobject *)(q-1); q->ob_type = NULL; return p + N_INTOBJECTS - 1; }
不同的PyIntBlock裡面的空閒的記憶體是怎樣連線起來構成free_list
的呢?這個祕密放在了整數物件垃圾回收的時候,在PyIntObject物件的tp_dealloc操作中可以看到:
[intobject.c] static void int_dealloc(PyIntObject *v) { if (PyInt_CheckExact(v)) { v->ob_type = (struct _typeobject *)free_list; free_list = v; } else v->ob_type->tp_free((PyObject *)v); }
原來PyIntObject物件銷燬時,它所佔用的記憶體並不會釋放,而是繼續被Python使用,進而將free_list
表頭指向了這個要被銷燬的物件上。
總結
- Python中的int物件就是c語言中long型別數值的擴充套件
- 小整數物件[-5, 257]在python中是共享的
- 整數物件都是從緩衝池中獲取的。
- 整數物件回收時,記憶體並不會歸還給系統,而是將其物件的ob_type指向free_list,供新建立的整數物件使用
原始碼參考:
關注公眾號「Python之禪」(id:vttalk)獲取最新文章