高性能場景下,HashMap的優化使用建議

laew4880 8年前發布 | 14K 次閱讀 高性能 JDK

如果不追求性能,這篇文章可以不看的,JDK本身已寫得足夠優秀,大家隨便用就好。

1. HashMap 在JDK 7 與 JDK8 下的差別

順便理一下HashMap.get(Object key)的幾個關鍵步驟,作為后面討論的基礎。

1.1 獲取key的HashCode并二次加工

因為對原Key的hashCode質量沒信心,怕會存在大量沖突,HashMap進行了二次加工。

JDK7的做法:

h ^= (h >>> 20) ^ (h >>> 12);

return h ^ (h >>> 7) ^ (h >>> 4);

JDK8 因為對自己改造過的哈希大量沖突時的紅黑樹有信心,所以簡單一些,只是把高16位異或下來。

return h ^ (h >>> 16);

所以即使Key比較均勻無哈希沖突,JDK8也比JDK7略快大原因大概于此。

順便科普一下,Integer的HashCode就是自己,Long要把高32位異或下來變成int, String則是累計結果*31+下一個字符,不過因為String是不可變對象,所以生成完一次就會自己cache起來。

1.2 落桶

index = hash & (array.length-1);

桶數組大小是2的倍數的好處,通過一次&就夠了,而不是代價稍大的取模。

1.3 最后選擇Entry

判斷Entry是否符合,都是首先哈希值要相等,但因為哈希值不是唯一的,所以還要對比key是否相等,最好是同一個對象,能用==對比,否則要走equals()。 比如String,如果不是同一個對象,equals()起來要一個個字符做比較也是挺累的。

if (e.hash == hash && ((k = e.key) == key || key.equals(k)))

return e.value;

更累的是存在哈希沖突的情況,比如兩個哈希值取模后落在同一個桶上,或者兩條不同的key有相同的哈希值。

JDK7的做法是建一條鏈表,后插入的元素在上面,一個個地執行上面的判斷。

而JDK8則在鏈表長度達到8,而且桶數量達到64時,建一棵紅黑樹,解決嚴重沖突時的性能問題。

2. 很多人忽視的加載因子Load Factor

加載因子存在的原因,還是因為減緩哈希沖突,如果初始桶為16,等到滿16個元素才擴容,某些桶里可能就有不止一個元素了。所以加載因子默認為0.75,也就是說大小為16的HashMap,到了第13個元素,就會擴容成32。

2.1 考慮加載因子地設定初始大小

相比擴容時只是System.arraycopy()的ArrayList,HashMap擴容的代價其實蠻大的,首先,要生成一個新的桶數組,然后要把所有元素都重新Hash落桶一次,幾乎等于重新執行了一次所有元素的put。

所以如果你心目中有明確的Map 大小,設定時一定要考慮加載因子的存在。

Map map = new HashMap(srcMap.size())這樣的寫法肯定是不對的,有25%的可能會遇上擴容。

Thrift里的做法比較粗暴, Map map = new HashMap( 2* srcMap.size()), 直接兩倍又有點浪費空間。

Guava的做法則是加上如下計算

(int) ((float) expectedSize / 0.75F + 1.0F);

2.2 減小加載因子

在構造函數里,設定加載因子是0.5甚至0.25。

如果你的Map是一個長期存在而不是每次動態生成的,而里面的key又是沒法預估的,那可以適當加大初始大小,同時減少加載因子,降低沖突的機率。畢竟如果是長期存在的map,浪費點數組大小不算啥,降低沖突概率,減少比較的次數更重要。

3. Key的設計

對于String型的Key,如果無法保證無沖突而且能用==來對比,那就盡量搞短點,否則一個個字符的equals還是花時間的。

甚至,對于已知的預定義Key,可以自己試著放一下,看沖不沖突。比如,像”a1”,”a2”,”a3” 這種,hashCode是個小數字遞增,絕對是不沖突的:)

4. EnumMap

對于上面的問題,有些同學可能會很沖動的想,這么麻煩,我還是換回用數組,然后定義一下下標的常量算了。其實不用自己來,EnumMap就是可讀性與性能俱佳的實現。

EnumMap的原理是,在構造函數里要傳入枚舉類,那它就構建一個與枚舉的所有值等大的數組,按Enum. ordinal()下標就能訪問數組。

美中不足的是,因為要實現Map接口,而 V get(Object key)中key是Object而不是泛型K,所以安全起見,它每次訪問都要先對Key進行類型判斷。在JMC里錄得不低的采樣命中頻率。所以也可以自己再port一個類出來,不實現Map接口,或者自己增加fastGet(),fastPut()的函數。

5. IntObjectHashMap

Netty以及其他 FastUtils 之類的原始類型map,都支持key是int或 long。但兩者的區別并不僅僅在于int 換 Integer的那點空間,而是整個存儲結構和Hash沖突的解決方法都不一樣。

HashMap的結構是 Node[] table; Node 下面有Hash,Key,Value,Next四個屬性。

而IntObjectHashMap的結構是int[] keys 和 Object[] values.

在插入時,同樣把int先取模落桶,如果遇到沖突,則不采樣HashMap的鏈地址法,而是用開放地址法(線性探測法)index+1找下一個空桶,最后在keys[index],values[index]中分別記錄。在查找時也是先落桶,然后在key[index++]中逐個比較key。

所以,對比整個數據結構,省的不止是int vs Integer,還有每個Node的內容。

而性能嘛,IntObjectHashMap還是穩贏一點的,隨便測了幾種場景,耗時至少都有24ms vs 28ms的樣子,好的時候甚至快1/3。

優化建議

  1. 考慮加載因子地設定初始大小
  2. 減小加載因子
  3. String類型的key,不能用==判斷或者可能有哈希沖突時,盡量減少長度
  4. 使用定制版的EnumMap
  5. 使用IntObjectHashMap

 

來自:http://calvin1978.blogcn.com/articles/hashmap.html

 

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