我認識的python(3)

渣渣強發表於2018-07-06

這一章主要說下python對內建物件管理採取的一些技巧。

整數物件池

像平時程式設計過程中,我們經常會使用整形資料,如果按照引用技術的方式,每次物件使用完,調整計數值並進行回收。對於常用的資料,這種做法很損害執行效率。所以在python中引入整形物件池管理機制進行整形物件的管理。
python中整型的定義如下所示:

我認識的python(3)

我認識的python(3)

我認識的python(3)
像tp_as_number定義了整形常用的函式簇。

大整數和小整數

整數的大小其實都是靠主觀判斷的。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整形的建立過程分為兩步:

  1. 如果小整數物件池機制被啟用了,則嘗試使用小整形物件池 small_ints
  2. 否則使用通用整形物件池(這個時候需要從某塊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

我認識的python(3)
假如當兩個PyIntBlock被建立,block_list預設指向最新的block結果,假如上一個block有物件引用為0需要回收,這個時候會將其加入到free_list中來。

我認識的python(3)

我認識的python(3)
以下該圖說明對於非常規整形,python不會對相同數字做唯一處理。
我認識的python(3)

字串優化

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(3)
通常python會建立一個PyStringObject物件temp後,呼叫PyString_InternInPlace對temp進行處理,然後減少temp的引用計數,temp物件就會因引用計數為0被回收,它只是臨時在記憶體出現。關於為什麼需要臨時建立PyStringObject是由於字典的鍵是該物件,被itern處理的字串分為兩種,一類是SSTATE_INTERNED_IMMORTAL狀態,另外一類是SSTATE_INTERNED_MORTAL狀態。後者會隨著虛擬機器的生命週期存活。

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對於的空間
複製程式碼

我認識的python(3)
dict也是有一個dict對應的快取池。dict插入邏輯大致是這樣的,首先判斷key是否是string,string由於具有ob_shash,可以優化。否則呼叫 P yObject_Hash(key)呼叫相應的函式計算出hash值,接下來進行insertdict操作,插入之後判斷是否需要對hash表進行擴容。(當使用空間大於總比分的3/4)

相關文章