HashMap 能有多快

2021-08-31 18:05:13 字數 3785 閱讀 1600

看到很多使用 map的**, 也有一些使用了 unordered_map或者 hash_map, 當然, hash_map 不是標準的, unordered_map 也只在 boost, tr1 和 c++0x 中可用. 從**的簡潔性和可移植性上講, 標準的 std::map 是首選.

然而, 從另一方面看, gcc 的 string 是 refcounted & copy on write 的, 64 位環境下, 乙個 string 的額外開銷是 32 位元組, 如果加上 string 內容的額外對齊(8 byte align)開銷, 則上公升到平均至少 36 位元組. 所以哪怕我的 string 簡單到只是 "a", 它佔的總記憶體是 32+8=40 位元組. 還有, 如果我們查詢時用的是 string literal, 也就是 smap["a"] 這樣的簡單用法, 系統會建立乙個 temp string, 然後傳遞過去.....

於是我打算自己寫乙個專門針對 key 為 string 的 hash map, 那天下午, 用了 2 個小時, 完成了乙個初步版本:

hashnode 定義如下:struct node ;string 的長度, 可以由陣列中兩個相鄰的 node.offset 相減獲得, 陣列最後包含乙個dummy node, 其 offset 指向 strpool 末尾.

另有乙個 fstring:

struct fstring fstring(const char* s, ptrdiff_t len) : p(s), n(len) {} fstring(const std::string& s) : p(s.data()), n(s.size()) {} };

加上內部的一些其它**, 如 hash function, equal function, .... 總共約 130 行, 當然, 這個實現的介面和標準 stl 容器有些差異.

寫了乙個測試程式, 分別對 map, unordered_map, 和我這個 hash_string 做測試. 結果讓人很吃驚: 針對不同的資料量, 我的 hash_strmap 比 map 快 30~40 倍, 比 unordered_map 快 5~8 倍, 32 位元組長的 key, 每秒鐘可以查詢20m次; 並且, 根據預估, 記憶體用量也比 map 和 unordered_map 小很多(map 每個結點有4ptr的空間開銷), 具體資料需要進一步測試.

看到這個令人鼓舞的結果, 我又花了很長一段時間, 將 hash_strmap 的介面實現得跟標準 stl 容器一致, 其中有乙個地方稍微麻煩一點:

標準的 *map, 其 value_type 是 std::pair(注意 value_type 和 value 是兩個東西), 而我這個實現, 其作為 key 的 string 是指向 strpool 的的乙個偏移, 雖然可以通過構造出 fstring(base+offset, len), 但是, value 怎麼辦, std::pair 不支援 reference, std::pair是非法的. 我的解決方法是, 將 value_type 定義成看上去象 std::pair 的乙個東西.

還有, 解決了 value_type 的問題, 作為 iterator, 需要實現operator* 和operator->, operator* 好實現, 只需要返回 value_type, 而非 value_type& 就可以可以了. operator-> 怎麼辦? 返回的指標不能指向乙個臨時物件, 那就只能把它放到 iterator 中, 但是這樣會增加 iterator 的尺寸, 而不管使用者是否使用 operator->, 並且會使 operator++ 和operator--複雜化. 有什麼解決的辦法?

當然有, c++ 標準規定: operator-> 並非必須返回乙個指標, 只要它返回的那個東西支援 operator-> 就可以了, 於是, 我把 iterator::pointer 定義成乙個物件, 然後...... 所有的問題都解決了.

還有乙個比較麻煩的問題: 元素刪除, 因為node放在陣列裡面, 陣列的元素刪除操作複雜度是 o(n), 這是無法接受的! 怎麼辦? 有兩個辦法:

1. 打標記, 因為 node.link >= -1, 只需要把它設成 < -1 的值就可以了, 然而這會在陣列中留下空洞

a. 這些空洞不影響查詢, 但是影響 iterator, iterate 時需要跳過空洞

2. 刪除時, 將陣列末尾的 node 移動到被刪除的地方, 並修改相應的 link, 將陣列大小減一, 這樣就沒有空洞, 但是會有另乙個問題, key string 的長度是由相鄰node.offset 相減獲得的, 移動 node 會破壞這個約定. 所以, 如果要實現這種策略, 就只能將 key string 的長度另外儲存, 或者存到 node 中, 或者存到 string 的內容之前.

a. 這個策略使得 iterator 非常容易實現, 並且 iterator 是 random_access iterator

3. strpool 中會留下空洞, 怎麼辦? 曾想過將這些空洞鏈結起來作為 freelist, 但是, 仍然有問題:

a. strpool 中的這些空洞長短不一, 是為每個不同的長度都分配乙個 list 呢? 還是放到同乙個list?

b. 放同乙個 list 查詢合適尺寸的塊會很慢

c. 放到不同的 list, 需要乙個 freelisthead 陣列, 而非單個元素

d. map 刪除元素一般用的比較少, 為乙個很少使用的特性, 付出太多, 值得嗎?

鑑於這些問題, 1, 2 兩種策略我都實現了, strpool 則不釋放空閒空間, 只有到空閒空間大於一定閾值時, 將它進行緊縮, 把那些空洞消除,對於策略1, 緊縮時, 將 node 陣列也一起緊縮.

內部支援 valueout, 也就是 value 和 node 不存放在相鄰的空間.

最後, 因為 node 是存放在陣列中的, 所以, 該 hash map 可以支援排序, 範圍查詢! 當然, 這裡的排序和範圍查詢是有侷限性的, 只有在 map 建立好之後, 並且不再插入和刪除. sort 和 lower_bound/upper_bound/equal_range 已經實現了, 當然, 排序之後需要重新鏈結以保證同時還能按 hash 進行精確查詢. 並且, 排序既可以按 key 排序, 也可以按 value 排序, 如果按 value 排序, valueout 可以提高二分查詢時的速度(記憶體訪問區域性性, 特別是到最後幾輪迴圈時).

還有一些其它細節問題, 以後有機會再寫.

最終的實現, 相比開始 130 行的**, 膨脹到了 1400 多行, 當然, 效率是相同的. iterator 的實現, 更是比 std::map 和 unorded_map 快了乙個數量級, 並且有非常好的記憶體訪問區域性性.

後來, 對 google sparsehashmap 中的那個效能測試**, 做了一點點改動, 使得它能接受我的 hash_strmap, 最終得到的結果也非常好, 幾乎每項操作都比參加測試的每種 map 都快, 最關鍵的插入/查詢操作則比其它所有map中最快的 map 還要快 3 倍以上, 並且, 因為那個效能測試並非為 stringkey 寫的, hash_strmap在這方面並沒有發揮自己的長處, 改天貼出詳細結果.

有了這個 hash_strmap, 在資料分析, 資料探勘中做 aggregation 時, 就不必擔心效能問題, 如果不犯其它低階錯誤, 就只有io問題. 最簡單的應用: wordcount, 按平均word 32位元組記,每秒鐘可以處理 32*20m = 640m 的資料, 對乙個低端的 8 核伺服器, 就是 640m*8=5120m, 如果 io+parse 有這麼快的話.

2011-09-28: 最新的測試達到了每秒 30m 的查詢.

2011-09-30: 測試 iteration 的速度, 比 std::map 快 150 倍以上, 比 unordered_map 快 130 倍, 這是當然的, 因為 node 是存放在陣列中的, 其它再怎麼快的遍歷, 能比遍歷陣列快?

記憶體究竟有多快

一般來說。cpu需要0個週期來訪問其暫存器,1 30個週期來訪問快取記憶體,50 200個週期來訪問主存。對於intel core i7來說。這個值可以很具體。intel core i7的主頻約在2 3ghz。可以計算出。l1 指令快取 l1 資料快取 l2 快取 l3 快取 記憶體訪問週期44 1...

XML解析到底能多快?

經驗總結 xml 解析到底能多快?by abllen 近來,因為工作的關係,需要對大xml檔案進行解析使用,就專門對解析效率問題作了下研究。場景 在我們的場景中,會將大量的業務配置資訊存放在db,便於前端修改,後端使用時,會先將db匯出到本地xml格式檔案,再各自載入。這裡之所以不直連資料庫一是考慮...

Ramdisk磁碟速度有多快?

一。用hd speed軟體測試 此處是read速度,寫效能需要清空乙個碟符,不好測 磁碟 50m s 記憶體盤 2.2g s 結論 讀相差40倍 二。自己寫軟體測試read write速度 很簡單,示例如下 using filestream fs new filestream this.textbo...