這一章主要說下python對內建物件管理採取的一些技巧。
整數物件池
像平時程式設計過程中,我們經常會使用整形資料,如果按照引用技術的方式,每次物件使用完,調整計數值並進行回收。對於常用的資料,這種做法很損害執行效率。所以在python中引入整形物件池管理機制進行整形物件的管理。
python中整型的定義如下所示:
大整數和小整數
整數的大小其實都是靠主觀判斷的。python 預設小整數的範圍在[-5, 257],這部分的整數是會存放到記憶體池快取,其他整數在python中會存放在一個公共區域。
以下的資料結構是維護python整型記憶體結構。用單向連結串列進行維護
#define BLOCK_SIZE 1000
#define BHEAD_SIZE 8
#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 PyIntBlock *block_free = NULL;
複製程式碼
python整形的建立過程分為兩步:
- 如果小整數物件池機制被啟用了,則嘗試使用小整形物件池 small_ints
- 否則使用通用整形物件池(這個時候需要從某塊block中的objects中找到一塊空閒的PyINtObject大小的空間)
static PyIntObject* fill_free_list(void){
PyIntObject *p, *q;
p = (PyObject*) PyMem_MALLOC(sizeof(PyIntBlock));
((PyIntBlock*)p) ->next = block_list;
block_list = (PyIntBlock *)p;
p = &((PyIntBlock*)p)->objects[0];
q = p+N_INOBJECTS;
while(--q > p){
q->ob_type = (struct _typeobject *)(q-1);
}
q->ob_type = NULL;
return p+N_INOBJECTS -1;
}
(free_list = fill_free_list()) == NULL,這裡將objects組織成連結串列並賦予free_list變數
v = free_list;
free_list = (PyIntObject *)v->ob_type;
這個過程是將free_list 指向下一個空閒空間,v則是分配出去的空間
複製程式碼
這裡使用ob_type去連線每一個int
假如當兩個PyIntBlock被建立,block_list預設指向最新的block結果,假如上一個block有物件引用為0需要回收,這個時候會將其加入到free_list中來。 以下該圖說明對於非常規整形,python不會對相同數字做唯一處理。字串優化
python中具有可變長度物件和不可變長度物件。對於不可變長度物件,我們沒辦法對其進行新增刪除操作。
由於字串本身具備不可變的特徵,使得字串可以作為字典的key值。string的定義如下所示:
typedef struct {
PyObject_VAR_HEAD
long ob_shash;
int ob_sstate;
char ob_sval[1]; /*字元指標*/
} PyStringObject
複製程式碼
ob_sstate表明該string有無被intern機制處理,ob_shash是快取hash值,避免重複計算。
初始化關鍵程式碼
op = (PyStringObject*)PyObject_MALLOC(sizeof(PyStringObject) + size)
PyObject_INIT_VAR(op, &PyString_Type, size);
op->ob_shash = -1;
op->ob_sstate = SSTATE_NOT_INTERN
if(str!=null){
memcpy(op->ob_sval, str, size)
}
op->ob_sval[size] = '\0';
...
return (PyObject*)op;
複製程式碼
intern機制:
目的讓系統中只存在唯一的字串對於的PyStringObject。
python針對單字元的做了類似小整形的處理,存在一個 static PyStringObject *characters[UCHAR_MAX+1]
list物件
typedef struct {
PyObject_VAR_HEAD
PyObject ** ob_item;
int allocated;
}
複製程式碼
allocated維護元素列表可容納的總數。
建立
PyObject* PyList_New(int size){
....
if(num_free_list) {
num_free_list --;
op = free_lists[num_free_list];
_Py_NewReference((PyObject*)op);
}else{
op = PyObject_Gc_New(PyListObject, &PyList_Type);
}
if (size<=0){
op->ob_item = NULL;
}else{
op->ob_item = (PyObject **)PyMem_MALLOC(nbytes);
memset(op-ob_item, 0, nbytes);
}
op->ob_size = size;
op->allocated = size;
return (PyObject*) op;
}
複製程式碼
num_free_list的來源
static void list_dealloc(PyListObject* op){
int i;
if(op->ob_item != NULL){
i = op->ob_size;
while(--i>0){
Py_XDECREF(op->ob_item[i])
}
PyMem_FREE(op->ob_item);
}
if(num_free_lists < MAXREFLISTS && PyList_CheckExtract(op)){
free_lists[num_free_lists++] = op;
}else{
op->ob_type->tp_free((PyObject*)op);
}
}
複製程式碼
dict
在c++的STL的map是採取RB-tree去實現一個雜湊結構,這樣子它的查詢開銷為O(log2N).而python實現dict是採取雜湊表。
使用雜湊表會存在衝突的可能性,像大學學的資料結構中有談及兩種常規的解決衝突的方式。連結串列法和開放定址法。python使用後者進行衝突解決。當然二次探測存在一個問題,當刪除的時候會存在斷鏈的問題,這個時候需要有一個機制繼續保持探測鏈的完整。所以python對刪除的key採取設定其狀態,使著不可用。
typedef struct {
Py_ssize_t me_hash;
PyObject* me_key;
PyObject* me_value;
} PyDictEntry
Entry表達三種狀態是這樣表達的:
當key和value都為null,代表Unused狀態
當key為dummy,value為null 代表Dummy狀態
當key有值且value不為null 代表Active狀態
#define PyDict_MINSIZE 8
typedef struct _dictobject PyDictObject;
struct _dictobject{
PyObject_HEAD
Py_ssize_t ma_fill;
Py_ssize_t ma_used;
Py_ssize_t ma_mask;
PyDictEntry *ma_tables;
PyDictEntry *(*ma_lookup)(PyObject *mp, PyObject *key, long hash);
PyDictEntry ma_smalltable[PyDict_MINSIZE];
}
當key的數量小於PyDict_MINSIZE,使用ma_smalltable儲存,超過之後使用ma_tables對於的空間
複製程式碼
dict也是有一個dict對應的快取池。dict插入邏輯大致是這樣的,首先判斷key是否是string,string由於具有ob_shash,可以優化。否則呼叫 P yObject_Hash(key)呼叫相應的函式計算出hash值,接下來進行insertdict操作,插入之後判斷是否需要對hash表進行擴容。(當使用空間大於總比分的3/4)