第五章-dict對象
Python裡的dict和C++ STL的map一樣,都是映射容器(key->value),但實現原理不同。由於Python內部大量使用dict這種結構(比如字串對象的intese機制),效率要求很高,所以Python沒有使用STL map的平衡二叉樹,而採用雜湊表,最低能在O(1)時間內完成搜尋。
使用hash就必須解決衝突的問題,dict採用的是開放定址法。原因我覺得是開放定址法比拉鏈法能更好地利用CPU cache,cache命中率較高。
探測函數為 i = (i << 2) + i + perturb + 1; perturb每探測一次就除以2^5。
dict的雜湊表裡每個slot都是一個自訂的entry結構:
typedef struct {
Py_ssize_t me_hash;
PyObject *me_key;
PyObject *me_value;
} PyDictEntry;
意義顧名思義,不多說了。
每個entry有三種狀態:Active, Unused, Dummy。
Unused:me_key == me_value == NULL,即未使用的空閑狀態。
Active:me_key != NULL, me_value != NULL,即該entry已被佔用
Dummy:me_key == dummy, me_value == NULL。
雜湊探測結束的條件是探測到一個Unused的entry。但是dict操作中必定會有刪除操作,如果刪除時僅把Active標記成Unused,顯然該entry之後的所有entry都不可能被探測到,所以引入了dummy結構。遇到dummy就說明當前entry處於空閑狀態,但探測不能結束。這樣就解決了刪除一個entry之後探測鏈斷裂的問題。
dict對象的定義為:
struct _dictobject {
PyObject_HEAD
Py_ssize_t ma_fill; /* # Active + # Dummy */
Py_ssize_t ma_used; /* # Active */
Py_ssize_t ma_mask;
PyDictEntry *ma_table;
PyDictEntry *(*ma_lookup)(PyDictObject *mp, PyObject *key, long hash);
PyDictEntry ma_smalltable[PyDict_MINSIZE];
};
ma_fill記錄Active + Dummy狀態的entry數。
ma_used記錄Active狀態的entry數。
ma_mask等於slot總數 - 1。因為一個key的雜湊值很可能超過slot總數,所以作為索引時得把它約束在slot總數的範圍內。而slot總數在定義的時候必須是2的乘冪,比如0x1000,所以減1之後就成了mask:0x111。再和hash做個&操作就能把索引之限制在0~0x111之間,即slot總數0x1000個,比較巧妙:)
ma_smalltable是預設的slot,初始有PyDict_MINSIZE個。
ma_table初始指向ma_smalltable,如果後期擴容,則指向新的slot空間。
ma_lookup為搜尋函數指標
dict對象的建立很簡單,先看看緩衝的對象池裡有沒有可用對象,如果有就直接用,沒有就從堆上申請。把fill和used域設成0。由於Python中把字串作為key的情況很多,所有搜尋函數就有一個針對string最佳化過的版本:lookdict_string。如果在檢查時發現key不是string對象,則調用預設的lookdict函數搜尋。
dict的插入操作由insertdict函數完成。插入操作的意義是:如果不存在key-value則插入,存在則覆蓋。所以先通過ma_lookup所指向的函數得到key所對應的entry。如果value不等於NULL,說明找到,將key指標替換。否則就直接在返回的entry上設定新的key-value對。
Python在處理d[key] = value這樣的運算式的時候調用的是insertdict函數的封裝函數PyDict_SetItem。PyDict_SetItem會計算key的雜湊值,然後把需要的資訊傳遞給insertdict。然後根據ma_table剩餘空間的大小決定是否resize。傳說和理論證明超過容量的2/3時衝突的機率大大增加,所以超過2/3後會進行擴容。
dict裡entry的刪除更簡單,算出雜湊值,找到entry,將其從Active轉換成Dummy,並調整table的容量。
最後是對象池。和前面list對象池一樣,dealloc時只回收table的記憶體,然後將dict放到池中,供後來new時再用。減少向堆申請記憶體的操作。