高性能Python-字典和集合

dewl1914 8年前發布 | 16K 次閱讀 Python 高性能 Python開發

當數據沒有明確的順序時,集合(sets)和字典(dictionaries)都是理想的數據結構,一個Key唯一對應一個存儲對象, Key可以是一個string,也可以是任意一個hashable的對象。

字典和集合的插入和查詢的時間復雜度是O(1),需要額外的內存開銷來支持,但是實際上,插入和查詢的時間取決于在用的hash函數。dictionaries是Key-Value的集合,sets是一個Key的集合。

插入和查詢

dictionaries和sets通過使用hash table達到O(1)的插入和查詢效率。對于一個hash表,我們必須搞清楚這段連續內存里都存放了什么,當我們插入新數據時,會產生兩個屬性,一個是Key的hash值,另一個是這個hash值和其他值的比較,當一個數據插入,Key首先被hashed和masked,key被轉化為一個List的高效索引。這個mask來確保,key的hash值可以為任意的整數,通過mask可以讓hash值縮小至分配的buckets的范圍內,不會越界,其實,一個mask就是一段可變的二進制值0b111, 0b11111,通過mask和hash值的位操作,截斷hash值。

例如,如果一個hash表只分配了8塊內存,只能存儲8個對象,一個key的hash值為28975,最后的index為28975 & 0b111 = 7

當然,我們需要檢查被插入的hash值對應的這塊內存的狀態,如果這塊內存為空,我們將hash值插入列表,把對象復制進內存塊,如果這塊內存為in use,內存塊里的值為value,那么該對象已經被添加過了,直接return,如果,內存塊里的值不為value,那么,我們必須找到新的位置去存放該對象。

找到新位置的方法稱為:probing,Python的probing機制引入原始hash值的高位bits因素,來幫助避免未來的hash沖突。另外, 一個評判hash表對應的數據分布情況的好壞的標準稱為‘load factor’,和hash函數的熵有關。

接下來,我們看一下Python字典如何根據一個Key來確定index:

可以參考Python的源代碼dictobject.c

def index_sequence(key, mask=0b111, PERTURB_SHIFT=5):
    perturb = hash(key)
    i = perturb & mask
    yield i
    while True:
        i = ((i << 2) + i + perturb + 1)
        perturb >>= PERTURB_SHIFT
        yield i & mask

上面的這個算法是linear probing的升級版,也是Python現在的機制,linear probing只能處理hash值的最后幾位(比如mask為0b111,只能截斷最后三位,意味著,當兩個key的最后三位是相同時,不僅會得到相同的hash值,而且,也會得到相同的index值,算法和計算順序無關的),對比后,Python的算法是擾亂的(perturb),每次計算index時,perturb都會變,也就是,相同的key的后幾位,由于不同的計算順序,返回不同的index,這處理了hash沖突。

別忘了我們能這么做的原因是,我們存儲了原始的Key值。當查詢時,通過key的hash值得到index,對比index中的key值和被查詢的key值是否一致,如果一致返回對象,如果不一致,繼續查詢。

Resizing

當更多的元素被添加到hash表時,hash表必須調整大小來適應。事實證明,當一個hash表只是 2/3 被利用時,hash沖突的概率是可以接受的。所以,當hash表的存儲達到這個點時,我們需要分配更大的hash表,更多的內存塊被分配,mask也調整為新的長度的mask,所有舊hash表中的數據被重新添加到新hash表中,因為你mask改變了,這個操作需要重新計算所有的index,所以, 調整一個大hash表的開銷是十分昂貴的

最小的dictionaries和sets的大小是8(參見Python源碼dictobject.c),也就是說,當你只存儲3個KeyValue時,Python的字典也會分配8個內存塊。

每次resize時,當小于50000個原色時,內存每次隨之遞增4倍,大于50000個時,遞增2倍,也就是說,無論字典和集合內存儲多少個元素,分配的內存只會是如下的size:8,32,128,512,2048,8192,32768...還有很重要的一點不要忘了,我們要保障hash表只有2/3的負載,來確保可以接受的hash碰撞率。所以,當一個字典有1039個元素時,必須至少分配1039 * 5 / 3 = 1731個buckets,所以根據規則,Python將分配2048個元素。

 

來自:http://www.jianshu.com/p/006fcc73ec07

 

 本文由用戶 dewl1914 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!