大型HashMap評估:JDK、FastUtil、Goldman Sachs、HPPC、Koloboke與Trove
介紹
這篇文章將會介紹以JDK中HashMap
作為標準的五種知名庫的hash map實現,并逐一測試:
- 原始數據類型到原始數據類型的映射
- 原始數據類型到對象的映射
- 對象到原始數據類型的映射
- 對象到對象的映射(JDK僅在這部分出現) </ul>
int
-int
int
-Integer
Integer
-int
Integer
-Integer
</ol>
- 準備:設置好一組int key和所需的填充因子
- 初始化一個map給定填充因子和容量=key的數量
- 填充map的key,設置values = keys
- 存儲一個key數組引用或者轉換為Integer[]型用以測試object key(否則會使用相同的key)
- 所有的測試幾乎是相同的——得到key數組所存儲的值然后再使用這些值,這樣JVM將不會優化你的代碼: </ol>
- Koloboke結果最好:使用
long[]
類型來存儲和一個不錯的技巧,將一個隨機自由的單元值復制給結果。Koloboke collections所有的3個版本都得到完全相同的結果(這并不意味著在其它測試中它們會一樣得快)。 - 用GS collections的實現速度排名是第二:使用2個數組而不是3個在這里獲得了不錯的代碼質量。
- FastUtil 和 HPPC性能表現完全相同的(相差不到不到2%)。
- 測試中Trove的實現最慢:在大多數映射的情況下,大概比Koloboke慢兩倍。在巨型映射(10M+)的情況下更慢。 </ol>
- Koloboke只使用了2個數組所以是最快的,對于空的單元而言代碼更簡單。
- FastUtil 和 HPPC緊隨GS collections身后(它們沒有盡力去發揮2個存儲數組的優勢而是使用了3個數組)。在不同的測試結果下它們略有不同,但彼此非常接近。
- Trove又是最慢的那個,比Koloboke要慢1.5至2倍。
- Koloboke略快于GS collections,但是GS collections在10K和10M大小上的測試略勝一籌。
- Trove和HPPC緊隨其后。
- FastUtil是本次測試最慢的那個——它比Koloboke慢2-2.5倍。有趣的是,雖然使用的都是底層數組=3的情況,但是HPPC的實現明顯比FastUtil要快。
- FastUtil和HPPC在每個映射中使用了3個數組,這沒啥新奇。
- .JDK中
HashMap
是存儲在Node
對象中的唯一項,這將鍵值進行了結合。那意味著每一項你至少有24個字節的開銷。而事實上則是32個字節的開銷,因為每一個bucket中的HashMap
是一個雙向鏈表,所以每一項有兩個額外的指針。 - Trove使用2個映射(和一個針對空單元的特殊哨兵對象)。
- 最后,GS collections 和 Koloboke正在使用一個交叉存儲的鍵值數組,這使它們成為了這6個Collection中CPU緩存利用得最好的一個。
- 通常情況下,Koloboke比JDK
HashMap
更快。但除了超大型map,其他情況即使Koloboke勝出,速度優勢也不是那么明顯。 - 對于大型和超大map,GS Collections已經非常接近Koloboke 和 JDKmaps的性能,但對于小型map測試結果相去甚遠了。
- 最后,FastUtil、HPPC和Trove對于各種大小的map下性能幾乎相同。
- Koloboke已經被證明是hash map實現最快也是存儲最高效的庫,但是它還剛剛誕生并沒有被廣泛使用,難道你不想試試?
- 如果你正在尋找更加穩定和成熟的庫(或者愿意犧牲部分性能),你或許應該看看GS collections library。與Koloboke不同,GS Collection給了你一個更加寬泛的開箱即用的集合。
這篇文章將會綜述一個測試——使用一組隨機key進行map讀取訪問(給定大小的待測試Collection共用同一組key)。
我們也會關注這些集合內的數據存儲方式和一些頗為有趣的實現細節。
評估對象
JDK 8
在這個測試中,JDK中的HashMap
是最古老的hash map實現。最近它有兩個主要的更新——一個在Java 7u40版本中對于空map的共享的底層存儲,以及在Java 8中將底層hash bucket鏈接成為哈希樹(改進更差情況下的性能)。
FastUtil 6.5.15
FastUtil為開發者提供了一套涵蓋上述四個方面的API(包括所有基本類型和對象的組合)。除了那些,這也有一些其它類型的映射適用于每一個參數的類型組合:數組映射、AVL樹映射和RB樹映射。然后,這篇文章中我們只對hash映射感興趣。
Goldman Sachs Collections 5.1.0
高盛集團在三年前公開了他的集合庫的源代碼。在我看來,(如果你有需要)這個庫提供最寬泛的開箱即用的集合。你應該注意你需要的不僅僅是一個hash map、tree map和你工作所需的其它東西。本文的目標,GS collections為每一個hash map提供了一個常規的、同步的和不可修改的版本。最后兩個僅僅提供了常規的映射,所以沒有什么性能優勢。
HPPC 0.6.1
HPPC為所有原始數據類型提供了數組列表、數組隊列、哈希集合和哈希映射。HPPC為原始數據類型key提供了普通的hash map,并且為object key提供了普通的和帶標識的hash map。
Koloboke 0.6
Koloboke是這篇文章中最年輕的庫。它是Roman Leventov開發的OpenHFT工程中的一部分。目前該庫為所有原始數據類型或對象的組合提供了hash map和hash set。這個庫最近被重命名成了HFTC,因此在一些我測試的實例中會使用一些舊的庫名。
Trove 3.0.3
Trove發布了很長的時間且十分穩定。不幸的是,現在這個項目已經沒有進一步的開發。Trove為所有的原始數據類型、對象的組合提供了鏈表、棧、隊列、hash set和hash map。我曾經寫過一篇關于Trove的文章。
數據存儲的實現和測試
本文將著眼于4種不同的map:
讓我們來看看這些map各自是如何存儲數據的。我們將參考測試命名而不是實際實現的名字。因為許多這些實現調用方式十分相似,同時也不容易通過命名來區分。在了解實現細節之后,我們將驗證它們是如何影響實際的測試結果。
我們將會使用JMH 1.0來測試。這里是測試描述:遍歷所有大小在(10K、100K、1M、10M和100M)的映射(外循環)從而生成一組隨機key(它們將在給定大小的映射中進行測試),然后運行每一個測試來實現映射(內循環)。每一個測試將會運行1億次 / map_size
(以便我們對每個測試實例調用map.get
1億次)。
public int runRandomTest() { int res = 0; for ( int i = 0; i < m_keys.length; ++i ) res = res ^ m_map.get( m_keys[ i ] ); return res; }
int-int
tests.maptests.primitive.FastUtilMapTest | int[] keys, int[] values, boolean[] used | </tr>|||||||||||||||||||||||||||||||||||||||||||||||||||
tests.maptests.primitive.GsMutableMapTest | int[] keys, int[] values | </tr>|||||||||||||||||||||||||||||||||||||||||||||||||||
tests.maptests.primitive.HftcMutableMapTest | long[] (key-low bits, value-high bits) | </tr>|||||||||||||||||||||||||||||||||||||||||||||||||||
tests.maptests.primitive.HppcMapTest | int[] keys, int[] values, boolean[] allocated | </tr>|||||||||||||||||||||||||||||||||||||||||||||||||||
tests.maptests.primitive.TroveMapTest | int[] _set, int[] _values, byte[] _states | </tr> </tbody> </table>
tests.maptests.prim_object.FastUtilIntObjectMapTest | int[] key, Object[] value, boolean[] used |
tests.maptests.prim_object.GsIntObjectMapTest | int[] keys, Object[] values |
tests.maptests.prim_object.HftcIntObjectMapTest | int[] keys, Object[] values |
tests.maptests.prim_object.HppcIntObjectMapTest | int[] keys, Object[] values, boolean[] allocated |
tests.maptests.prim_object.TroveIntObjectMapTest | int[] _set, Object[] _values, byte[] _states |
不出意外,FastUtil、HPPC 和 Trove使用了3個數組(包括一個單元狀態的數組)。GS collections 和 Koloboke使用了2個數組,這個技巧類似于上面列出的特殊情況。
int-Object測試結果


本次測試結果分三組(從快至慢):
Object-int
tests.maptests.object_prim.FastUtilObjectIntMapTest | Object[] key, int[] value, boolean[] used |
tests.maptests.object_prim.GsObjectIntMapTest | Object[] keys, int[] values |
tests.maptests.object_prim.HftcObjectIntMapTest | Object[] keys, int[] values |
tests.maptests.object_prim.HppcObjectIntMapTest | Object[] keys, int[] values, boolean[] allocated |
tests.maptests.object_prim.TroveObjectIntMapTest | Object[] _set, int[] _values |
對于Object key,FastUtil和HPPC使用了第三個數組。這看上去并不是什么好主意。因為你可以允許使用一個私有的哨兵對象作為object key的一個標志。實際上它的性能略差。
GS collections、Koloboke 和 Trove使用的是2個數組,所以有理由期待它們會快一點。
Object-int測試結果


這里分三組再次進行測試,但是不像之前那樣特意區分(由快到慢):
Object-Object
tests.maptests.object.FastUtilObjMapTest | Object[] keys, Object[] values, boolean[] used |
tests.maptests.object.GsObjMapTest | Object[] table – interleaved keys and values |
tests.maptests.object.HftcMutableObjTest | Object[] tab – interleaved keys and values |
tests.maptests.object.HppcObjMapTest | Object[] keys, Object[] values, boolean[] allocated |
tests.maptests.object.JdkMapTest | Node<K,V>[] table – each Node could be a part of a linked list or a TreeMap (Java 8) |
tests.maptests.object.TroveObjMapTest | Object[] _set, Object[] _values |
在Object-Object的映射的結果情況有些復雜:
現在,對這些map的實現了解之后。讓我們開始測試這些map的性能。
Object-Object測試結果


Object-Object測試結果更為復雜。
10億項測試
我將創建一個大小為10億項(entry 鍵值對),然后看看這些集合會發生什么。測試中設置因子為0.5,這意味著所有這些映射必須分配一個非常大的數組,允許的最大長度接近231。
FastUtil、HPPC和GS collections因為各種異常失敗了(并不是因為OOM——我為這次測試分配了110G的內存)。
Koloboke、Trove和JDK設法通過了這些測試。不幸的是,這些測試在JMH上卻沒有成功運行。他們運行了另外一套代碼。
以上就是測試的結果(如果需要和之前的結果進行比較,需要把之前的結果乘以10。因為所有之前的結果總共調用了100M次map.get
):
tests.maptests.primitive.HftcMutableMapTest : time = 95.05 sec tests.maptests.primitive.TroveMapTest : time = 235.062 sec tests.maptests.prim_object.HftcIntObjectMapTest : time = 216.361 sec tests.maptests.prim_object.TroveIntObjectMapTest : time = 304.019 sec tests.maptests.object_prim.HftcObjectIntMapTest : time = 335.139 sec tests.maptests.object_prim.TroveObjectIntMapTest : time = 217.412 sec tests.maptests.object.HftcMutableObjTest : time = 272.792 sec tests.maptests.object.JdkMapTest : time = 163.335 sec tests.maptests.object.TroveObjMapTest : time = 239.133 sec
如你所見,Koloboke在primitive-primitive的轉化上贏得了很大的優勢。在primitive-to-object的轉換測試方面也明顯快了不少。
在object-primitive轉換測試中,Koloboke就明顯比Trove用時要久。
最后,對于object-object的轉換測試,我必須改變Koloboke map的初始化代碼,因為一旦我添加5億個元素性能就開始迅速下降:
HashObjObjMaps.getDefaultFactory().withHashConfig(HashConfig.fromLoads(0.5, 0.6, 0.8)).newMutableMap(keys.length)
Koloboke 2.0?
Roma Leventov剛剛宣布他正在考慮建一個更新更快的Koloboke庫,但是這需要你的反饋。你能幫他給出一點建議嗎?