Lua 源碼分析之Table - Hash部分內部原理

Lua設計裏面,Table是一個特別關鍵的部分。它可以表示很多的數據結構,可以是Array,可以是Map,可以根據自己的需要實現棧,隊列等等,使用起來方便。源碼裏面的設計顯得特別重要了,它是被很頻繁地使用,提高性能是設計者重中之中。

首先先看一下Lua的總體設計:它分爲兩部分,分別是數組Array和Hash部分。數組部分主要是存儲下標從1開始的連續不爲空的節點內容,如果是中間斷開部分會存到hash部分。Hash部分是存儲各種類型的離散數據。如下圖

Table內部結構概覽圖

 

1.對於一個Table初始化的時候,如果是空表,即Array和Hash部分長度都爲0;下面着重看Hash部分
    lua代碼: local tb = {};
    c代碼: lua_newtable(L);
    t->lsize = 0;
    t->lsizenode = 0; //對數值,用來計算2的冪,結果爲當前內存分配node數量(包括空的或使用的)
    t->node = dummynode; //內存的頭,在分配內存時,分配了一個數量爲lsizenode連續的Node類型的內存塊
    t->lastfree = gnode(t,0); //內存的尾部,實際爲dummynode
    假設我們現在初始Hash部分長度爲2,2的指數,即爲4個,會生成長度爲4個節點,每個節點內容都爲空,next也置爲空(實        際作用是作爲一個相同hash值的key的鏈表),初始化後的結構如下:

Hash部分初始化後的結構圖

 

2.現在我們要給Table賦值,例如執行:tb["k0"] = "v0", key爲"k0", value爲:"v0"
(1)根據key去查找有沒有已經存在的。
(2)如果有,更新一下value即可。(顯然,現在是沒有的)
(3)如果沒有,需要newkey(hash部分最最最核心的函數), newkey過程:
a.檢查需要查找的key的mainposition是否爲空;(mainposition實際爲根據key的hash值計算得出來一個Node)
b.如果mainposition是可用並且是空的(在t->node指向的內存空間內找到沒有佔用的Node),就使用它,設置key和value。(這個時候,4個節點都爲空,可以設置了)。假設它找到的位置是第3個,此時結構圖爲:注意,lastfree並不是在這個時候移動的。

3.現在賦值: tb["k1"] = "v1",通過key查找mainposition的時候,回到剛上一步newkey的過程。此時假如查找"k1"的mainposition和"k0"的一樣時候,這時"k1"發現自己的mainposition被佔用了。既然被佔用了,那我總得找個地方安放吧?好,那我去找一個空的坑填一下。怎麼找?通過lastfree從後往前找,一個一個找。根據此時內存結構,應該找的到下標爲2的節點,同時lastfree會停留在這裏。既然"k0"佔了"k1"的位置,"k1"要想辦法協調了。要檢查"k0"的mainposition到底是不是現在的位置。如果是的話,那就是"兄弟了"(兩個key通過hash計算出來的mainposition一樣),如果不是就需要讓位了。如果是"兄弟",那就將就下,但是要通過鏈表連接起,表明一下“兄弟”關係。把"k1"指向"k0"的下一個節點,再把“k0”的next指向“k1”,此時內存結構圖爲:

4.再賦值多一次(我也不想啊,一定要操作這麼多次才能把情況說完),tb["k2"] = "v2"。目前的情況是:k0是在自己的mainposition上面,k1不是在自己的mainposition(這個位置不屬於k1自己的小天地)。此時,“k2”來搞事了,它計算得出來,“k1”所在的節點是"k2"的mainposition。既然不是“兄弟”,那就把屬於自己的東西拿回來啊。拿歸拿,先找一個freepos來談判。通過lastfree繼續向前找,找到下標爲1的節點,lastfree此時停留在1的位置。好了,找到位置了,然後呢,讓"k1"自己搬過去空位置去,同時讓“k0”的下一個繼續指向“k1”所在的位置,這樣子,把"k1"的mainposition騰出來了,大家都樂得其所。此時內存結構圖爲:

5.再來再來,還沒完。假設現在賦值, tb["k3"] = "v3",剛好"k3"毫無差池地落入了第0個裏,好了,剛好填空坑了。

6.你以爲這樣就完了嗎?怎麼可能呢。這個時候,再來一個 tb["k4"] = "v4"。噢,找不位置了,一個位置都沒有了。怎麼辦?擴容啊,麻煩給我擴大點。擴大的規則是:根據當前lsizenode來增加,當前指數爲2,2的2次方,爲4。在這個基礎上指數增加一級,就是lsizenode即爲3了,變成2的3次,擴容後數量爲8,亦即爲rehash。rehash的過程是:先分配一塊新的內存塊,像這裏的情況是分配了長度爲8的節點大小內存塊。此時,要把舊的遷移過來新內存塊。但是,它並不是按順序一個一個對位拷貝的,遍歷每一個節點的時候,需要重新去找一次mainposition,所以分配前後的位置可能會不一樣(它和Array部分不一樣,Array部分是直接一對一,位置不變地拷貝的)。這個時候有rehash之後可能是(注意!這僅僅是可能,只是要表達,它的位置需要重新找):

rehash以後,再重新去找"k4"的位置,重複第3步的邏輯。


用和源碼比較一致的邏輯來複習一遍newkey的過程:
1.檢查需要查找的key的mainposition是否爲空;(mainposition實際爲根據key的hash值計算得出來一個Node)
2.如果mainposition是可用並且是空的(在t->node指向的內存空間內找到沒有佔用的Node),就使用它,設置key和value。
3.如果mainposition是被佔用着或是dummynode(如果是新建的表,第一次賦值,必定是dummynode),記這個mainposition爲mp, 則需要走下面的流程
    a.在內存塊裏查一個空節點:從最後往前找(即是從lastfree開始往node方向找,地址大小遞減)
    b.如果找不到空節點,表明:當前所有node都已經用完了,需要rehash重新分配一塊大的內存,重新再跑一次set的流程。
    c.如果找到了,記這個空節點爲n, 此時記錄有mp(key自己應該所屬的位置),和一個空節點n。
        i.首先把mp已經把已經佔有的值的key拿出來再計算一次它的mainposition,記爲othern,看看othern是不是也是它自己應該佔的位置
        ii.如果不是,就是原來佔用的node丟到空節點n那裏去,讓mp佔回屬於自己應該佔的位置。
        iii.如果是,就把mp節點放到othern單向鏈表裏,並且是othern下一個,再把mp的下一個節點指向othern原來的下一個(簡單來說就是把mp插到othern的前面去,othern這個鏈表的實際意義是:根據key計算出來的mp都一樣,就把它做成一個鏈表,在hash有衝突的時候可以在這個鏈表裏查找,提高性能)

整個Table的hash部分是最爲關鍵核心的,需要理清楚實際的內部原理。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章