簡體   English   中英

Java HashMap性能優化/替代方案

[英]Java HashMap performance optimization / alternative

我想創建一個大的HashMap,但put()性能不夠好。 有任何想法嗎?

其他數據結構建議是受歡迎的,但我需要Java Map的查找功能:

map.get(key)

在我的情況下,我想創建一個包含2600萬條目的地圖。 使用標准Java HashMap,在2-3百萬次插入后,放置速率變得無法忍受。

此外,是否有人知道為密鑰使用不同的哈希代碼分發是否有幫助?

我的哈希碼方法:

byte[] a = new byte[2];
byte[] b = new byte[3];
...

public int hashCode() {
    int hash = 503;
    hash = hash * 5381 + (a[0] + a[1]);
    hash = hash * 5381 + (b[0] + b[1] + b[2]);
    return hash;
}

我使用add的associative屬性來確保相等的對象具有相同的哈希碼。 數組是字節,其值在0到51之間。值只在一個數組中使用一次。 如果a數組包含相同的值(按任意順序),則對象相等,而b數組則相同。 所以a = {0,1} b = {45,12,33}和a = {1,0} b = {33,45,12}是相等的。

編輯,一些說明:

  • 一些人批評使用哈希映射或其他數據結構來存儲2600萬個條目。 我不明白為什么這看起來很奇怪。 它看起來像是一個經典的數據結構和算法問題。 我有2600萬個項目,我希望能夠快速將它們插入並從數據結構中查找它們:給我數據結構和算法。

  • 將默認Java HashMap的初始容量設置為2600萬會降低性能。

  • 有些人建議使用數據庫,在某些其他情況下這絕對是明智的選擇。 但我真的在問一個數據結構和算法的問題,一個完整的數據庫會比一個好的數據結構解決方案過度而且速度慢得多(畢竟數據庫只是軟件,但會有通信和可能的磁盤開銷)。

正如許多人指出的那樣, hashCode()方法應該受到指責。 它只為2600萬個不同的對象生成了大約20,000個代碼。 這是每個哈希桶平均1,300個對象=非常非常糟糕。 但是,如果我將兩個數組轉換為基數52中的數字,我保證會為每個對象獲取一個唯一的哈希碼:

public int hashCode() {       
    // assume that both a and b are sorted       
    return a[0] + powerOf52(a[1], 1) + powerOf52(b[0], 2) + powerOf52(b[1], 3) + powerOf52(b[2], 4);
}

public static int powerOf52(byte b, int power) {
    int result = b;
    for (int i = 0; i < power; i++) {
        result *= 52;
    }
    return result;
}

對數組進行排序以確保此方法滿足hashCode()協定,即等於對象具有相同的哈希代碼。 使用舊方法,每秒平均放置次數超過100,000次放置,100,000到2,000,000是:

168350.17
109409.195
81344.91
64319.023
53780.79
45931.258
39680.29
34972.676
31354.514
28343.062
25562.371
23850.695
22299.22
20998.006
19797.799
18702.951
17702.434
16832.182
16084.52
15353.083

使用新方法給出:

337837.84
337268.12
337078.66
336983.97
313873.2
317460.3
317748.5
320000.0
309704.06
310752.03
312944.5
265780.75
275540.5
264350.44
273522.97
270910.94
279008.7
276285.5
283455.16
289603.25

好多了。 舊方法很快就停止了,而新方法保持了良好的吞吐量。

我在hashCode()方法中注意到的一件事是數組a[]b[]元素的順序無關緊要。 因此(a[]={1,2,3}, b[]={99,100})將散列為與(a[]={3,1,2}, b[]={100,99})相同的值(a[]={3,1,2}, b[]={100,99}) 實際上,所有密鑰k1k2 ,其中sum(k1.a)==sum(k2.a)sum(k1.b)=sum(k2.b)將導致沖突。 我建議為數組的每個位置分配一個權重:

hash = hash * 5381 + (c0*a[0] + c1*a[1]);
hash = hash * 5381 + (c0*b[0] + c1*b[1] + c3*b[2]);

其中, c0c1c3不同的常量(如果需要,可以對b使用不同的常量)。 這應該可以使事情變得更加平坦。

詳細說明Pascal:你了解HashMap的工作原理嗎? 您的哈希表中有一些插槽。 找到每個密鑰的哈希值,然后映射到表中的條目。 如果兩個哈希值映射到同一條目 - “哈希沖突” - HashMap構建鏈接列表。

散列沖突可以破壞哈希映射的性能。 在極端情況下,如果您的所有密鑰都具有相同的哈希碼,或者如果它們具有不同的哈希碼但它們都映射到同一個槽,那么您的哈希映射將變為鏈接列表。

因此,如果您看到性能問題,我要檢查的第一件事是:我是否獲得了隨機分布的哈希碼? 如果沒有,您需要更好的哈希函數。 那么,在這種情況下“更好”可能意味着“更好地為我的特定數據集”。 比如,假設您正在使用字符串,並且您將字符串的長度用於哈希值。 (不是Java的String.hashCode如何工作,但我只是編寫一個簡單的例子。)如果你的字符串有很大的變化長度,從1到10,000,並且相當均勻地分布在這個范圍內,這可能是一個非常好的哈希函數。 但是如果你的字符串都是1或2個字符,那么這將是一個非常糟糕的哈希函數。

編輯:我應該添加:每次添加新條目時,HashMap都會檢查這是否重復。 當存在哈希沖突時,它必須將傳入密鑰與映射到該槽的每個密鑰進行比較。 因此,在最糟糕的情況下,所有內容都散列到一個插槽,第二個鍵與第一個鍵進行比較,第三個鍵與#1和#2進行比較,第四個鍵與#1,#2和#3進行比較當你獲得關鍵#100萬時,你已經完成了超過一萬億的比較。

@Oscar:嗯,我看不出那是“不是真的”。 這更像是“讓我澄清”。 但是,是的,如果您使用與現有條目相同的密鑰創建新條目,則會覆蓋第一個條目。 當我談到在最后一段中查找重復時,這就是我的意思:每當一個鍵哈希到同一個槽時,HashMap必須檢查它是否是現有密鑰的副本,或者它們是否只是在同一個槽中的巧合哈希函數。 我不知道那是HashMap的“重點”:我會說“整點”是你可以快速按鍵檢索元素。

但無論如何,這並不影響我試圖制作的“整點”:當你有兩個鍵 - 是的,不同的鍵,而不是同一個鍵再次顯示 - 映射到表中的同一個插槽,HashMap構建一個鏈表。 然后,因為它必須檢查每個新密鑰以查看它是否實際上是現有密鑰的副本,所以每次嘗試添加映射到同一個槽的新條目都必須追蹤鏈接列表,檢查每個現有條目以查看是否是先前看到的密鑰的副本,或者它是否是新密鑰。

在原帖后很久更新

在發布6年后,我剛剛對這個答案進行了投票,這讓我重新閱讀了這個問題。

問題中給出的哈希函數對於2600萬個條目來說不是一個好的哈希。

它將[0] + a [1]和b [0] + b [1] + b [2]加在一起。 他說每個字節的值范圍從0到51,因此只給出(51 * 2 + 1)*(51 * 3 + 1)= 15,862個可能的哈希值。 有2600萬個條目,這意味着每個哈希值平均大約有1639個條目。 這是很多很多的沖突,需要通過鏈表進行大量的連續搜索。

OP表示陣列a和陣列b中的不同順序應該被認為是相等的,即[[1,2],[3,4,5]]。等於([[2,1],[5,3,4] ]),為了履行合同,他們必須有相同的哈希碼。 好的。 盡管如此,仍有超過15,000個可能的值。 他的第二個提議哈希函數要好得多,給出了更廣泛的范圍。

雖然正如其他人所評論的那樣,哈希函數似乎不適合更改其他數據。 在創建對象時“標准化”對象或使對象的副本使用散列函數更有意義。 此外,每次通過函數使用循環來計算常量是低效的。 由於這里只有四個值,我會寫

return a[0]+a[1]*52+b[0]*52*52+b[1]*52*52*52+b[2]*52*52*52*52;

這會導致編譯器在編譯時執行一次計算; 或者在類中定義4個靜態常量。

此外,散列函數的初稿有幾個計算,無法添加到輸出范圍。 注意,在考慮類中的值之前,他首先設置hash = 503而不是乘以5381。 所以...實際上他為每個值添加了503 * 5381。 這取得了什么成果? 為每個哈希值添加一個常量只會燒掉cpu周期而不會完成任何有用的操作。 這里的教訓:增加哈希函數的復雜性不是目標。 目標是獲得廣泛的不同價值,而不僅僅是為了復雜性而增加復雜性。

我的第一個想法是確保你正確地初始化你的HashMap。 JavaDocs for HashMap

HashMap的一個實例有兩個影響其性能的參數:初始容量和負載因子。 容量是哈希表中的桶數,初始容量只是創建哈希表時的容量。 加載因子是在自動增加容量之前允許哈希表獲取的完整程度的度量。 當哈希表中的條目數超過加載因子和當前容量的乘積時,哈希表將被重新哈希(即,重建內部數據結構),以便哈希表具有大約兩倍的桶數。

因此,如果您從一個太小的HashMap開始,那么每次需要調整大小時, 所有哈希都會被重新計算...這可能是您在達到2-3百萬個插入點時所感受到的。

我建議采取三管齊下的方法:

  1. 運行具有更多內存的Java:例如java -Xmx256M以256兆字節運行。 如果需要,可以使用更多,並且有大量的RAM。

  2. 按照另一張海報的建議緩存計算的哈希值,因此每個對象只計算一次哈希值。

  3. 使用更好的散列算法。 您發布的那個將返回相同的散列,其中a = {0,1},而a = {1,0},其他所有相等。

利用Java為您提供的免費服務。

public int hashCode() {
    return 31 * Arrays.hashCode(a) + Arrays.hashCode(b);
}

我很確定這比現有的hashCode方法碰撞的可能性要小得多,盡管它取決於數據的確切性質。

進入“開/關主題”的灰色區域,但有必要消除有關Oscar Reyes建議更多哈希沖突是一件好事的混淆,因為它減少了HashMap中的元素數量。 我可能會誤解奧斯卡所說的話,但我似乎並不是唯一一個:kdgregory,delfuego,Nash0,我似乎都有同樣的(錯誤的)理解。

如果我理解Oscar對同一個具有相同哈希碼的類的說法,他建議只有一個具有給定哈希碼的類的實例將插入到HashMap中。 例如,如果我有一個哈希碼為1的SomeClass實例和一個哈希碼為1的SomeClass的第二個實例,則只插入一個SomeClass實例。

http://pastebin.com/f20af40b9上的Java pastebin示例似乎表明上面正確總結了Oscar提出的建議。

無論是否有任何理解或誤解,如果同一類的不同實例具有相同的哈希碼,則不會僅將其插入HashMap中 - 直到確定鍵是否相等為止。 哈希碼契約要求相等的對象具有相同的哈希碼; 但是,它並不要求不相等的對象具有不同的哈希碼(盡管出於其他原因這可能是合乎需要的)[1]。

下面是pastebin.com/f20af40b9示例(Oscar至少引用兩次),但略微修改以使用JUnit斷言而不是printlines。 此示例用於支持相同哈希碼導致沖突的提議,並且當類相同時,僅創建一個條目(例如,在此特定情況下僅一個字符串):

@Test
public void shouldOverwriteWhenEqualAndHashcodeSame() {
    String s = new String("ese");
    String ese = new String("ese");
    // same hash right?
    assertEquals(s.hashCode(), ese.hashCode());
    // same class
    assertEquals(s.getClass(), ese.getClass());
    // AND equal
    assertTrue(s.equals(ese));

    Map map = new HashMap();
    map.put(s, 1);
    map.put(ese, 2);
    SomeClass some = new SomeClass();
    // still  same hash right?
    assertEquals(s.hashCode(), ese.hashCode());
    assertEquals(s.hashCode(), some.hashCode());

    map.put(some, 3);
    // what would we get?
    assertEquals(2, map.size());

    assertEquals(2, map.get("ese"));
    assertEquals(3, map.get(some));

    assertTrue(s.equals(ese) && s.equals("ese"));
}

class SomeClass {
    public int hashCode() {
        return 100727;
    }
}

但是,哈希碼並不是完整的故事。 pastebin示例忽略的是sese都相等的事實:它們都是字符串“ese”。 因此,使用sese"ese"作為鍵插入或獲取地圖的內容都是等效的,因為s.equals(ese) && s.equals("ese")

第二個測試表明,在同一個類中使用相同的哈希碼是錯誤的,因為當在測試一中調用map.put(ese, 2)時,鍵 - >值s -> 1ese -> 2覆蓋。 在測試二中, sese仍然具有相同的哈希碼(由assertEquals(s.hashCode(), ese.hashCode()); )並且它們是同一個類。 但是, sese是此測試中的MyString實例,而不是Java String實例 - 與此測試相關的唯一區別是equals: String s equals String ese上面測試一中的String s equals String ese ,而MyStrings s does not equal MyString ese在測試二MyStrings s does not equal MyString ese

@Test
public void shouldInsertWhenNotEqualAndHashcodeSame() {
    MyString s = new MyString("ese");
    MyString ese = new MyString("ese");
    // same hash right?
    assertEquals(s.hashCode(), ese.hashCode());
    // same class
    assertEquals(s.getClass(), ese.getClass());
    // BUT not equal
    assertFalse(s.equals(ese));

    Map map = new HashMap();
    map.put(s, 1);
    map.put(ese, 2);
    SomeClass some = new SomeClass();
    // still  same hash right?
    assertEquals(s.hashCode(), ese.hashCode());
    assertEquals(s.hashCode(), some.hashCode());

    map.put(some, 3);
    // what would we get?
    assertEquals(3, map.size());

    assertEquals(1, map.get(s));
    assertEquals(2, map.get(ese));
    assertEquals(3, map.get(some));
}

/**
 * NOTE: equals is not overridden so the default implementation is used
 * which means objects are only equal if they're the same instance, whereas
 * the actual Java String class compares the value of its contents.
 */
class MyString {
    String i;

    MyString(String i) {
        this.i = i;
    }

    @Override
    public int hashCode() {
        return 100727;
    }
}

根據后來的評論,奧斯卡似乎扭轉了他早先所說的話,並承認平等的重要性。 然而,似乎仍然認為平等是重要的,而不是“同一類”,不清楚(強調我的):

“不是真的。只有當哈希值相同但密鑰不同時才創建列表。例如,如果String給出哈希碼2345並且Integer給出相同的哈希碼2345,那么整數就會插入到列表中,因為String。 equals(Integer)為false。但如果你有相同的類(或者至少.equals返回true)則使用相同的條目。例如new String(“one”)和`new String(“one”)用作密鑰,將使用相同的條目。實際上這是HashMap的第一個要點!親眼看看:pastebin.com/f20af40b9 - Oscar Reyes“

與之前的注釋明確地解決了相同類和相同哈希碼的重要性,沒有提到equals:

“@delfuego:自己看看:pastebin.com/f20af40b9所以,在這個問題中,正在使用相同的類(等一下,正在使用同一個類嗎?)這意味着當使用相同的哈希相同的條目時使用,並沒有條目的“列表”。 - 奧斯卡雷耶斯“

要么

“實際上,這會增加性能。更多的沖突eq更少的哈希表eq中的條目。更少的工作要做。不是哈希(看起來很好),也不是哈希表(它工作得很好)我敢打賭它是在對象上表演性能下降的創作。 - Oscar Reyes“

要么

“@kdgregory:是的,但是只有當碰撞發生在不同的類中時,對於同一個類(在這種情況下)才會使用相同的條目。 - Oscar Reyes”

我可能會誤解奧斯卡實際上想說的話。 然而,他最初的評論引起了足夠的混淆,通過一些明確的測試清除所有內容似乎是謹慎的,因此沒有揮之不去的疑慮。


[1] - 來自Effective Java,第二版由Joshua Bloch撰寫:

  • 每當在執行應用程序期間多次在同一對象上調用它時,hashCode方法必須始終返回相同的整數,前提是不修改在對象的相等比較中使用的信息。 從應用程序的一次執行到同一應用程序的另一次執行,該整數不需要保持一致。

  • 如果兩個對象根據等於s(Obj ect)方法相等,則在兩個對象中的每一個上調用hashCode方法必須產生相同的整數結果。

  • 如果兩個對象根據相等的s(Object)方法不相等,則不需要在兩個對象中的每一個上調用hashCode方法必須產生不同的整數結果。 但是,程序員應該知道為不等對象生成不同的整數結果可能會提高哈希表的性能。

如果發布的hashCode中的數組是字節,那么最終可能會出現大量重復數據。

a [0] + a [1]將始終介於0和512之間。添加b將始終生成介於0和768之間的數字。將這些數字相乘並獲得400,000個唯一組合的上限,假設您的數據是完美分布的在每個字節的每個可能值之間。 如果您的數據完全正常,則此方法的唯一輸出可能要少得多。

HashMap具有初始容量,HashMap的性能非常依賴於生成底層對象的hashCode。

嘗試調整兩者。

如果鍵具有任何圖案,則可以將地圖拆分為較小的地圖並具有索引圖。

示例:密鑰:1,2,3,.... n 28個地圖,每個100萬。 索引地圖:1-1,000,000 - > Map1 1,000,000-2,000,000 - > Map2

因此,您將進行兩次查找,但密鑰集將為1,000,000與28,000,000。 您也可以使用刺痛模式輕松完成此操作。

如果密鑰是完全隨機的,那么這將不起作用

如果您提到的兩個字節數組是您的整個鍵,則值在0-51范圍內,唯一且a和b數組中的順序無關緊要,我的數學告訴我,只有大約2600萬個可能的排列和您可能正在嘗試使用所有可能鍵的值填充地圖。

在這種情況下,如果使用數組而不是HashMap並將其從0到25989599索引,那么從數據存儲中填充和檢索值當然會快得多。

我遲到了,但有幾條關於大地圖的評論:

  1. 正如在其他帖子中詳細討論的那樣,使用一個好的hashCode(),Map中的26M條目沒什么大不了的。
  2. 但是,這里潛在的隱藏問題是巨型地圖對GC的影響。

我假設這些地圖是長壽的。 即你填充它們並在應用程序的持續時間內保持不變。 我也假設應用程序本身很長壽 - 就像某種服務器一樣。

Java HashMap中的每個條目都需要三個對象:鍵,值和將它們連接在一起的Entry。 因此,地圖中的26M條目意味着26M * 3 == 78M對象。 這很好,直到你達到一個完整的GC。 然后你有一個暫停世界的問題。 GC將查看每個78M對象並確定它們都是活着的。 78M +對象只是很多要查看的對象。 如果您的應用程序可以忍受偶爾的長時間(可能是幾秒鍾)暫停,那就沒有問題。 如果您正在嘗試實現任何延遲保證,那么您可能會遇到一個重大問題(當然,如果您想要延遲保證,Java不是選擇的平台:))如果地圖中的值快速流失,您最終可能會頻繁完全收集這大大加劇了這個問題。

我不知道這個問題有很好的解決方案。 思路:

  • 有時可以調整GC和堆大小以“大部分”阻止完整的GC。
  • 如果您的地圖內容流失很多,您可以嘗試Javolution的FastMap - 它可以匯集Entry對象,這可以降低完全收集的頻率
  • 您可以創建自己的map impl並在byte []上進行顯式內存管理(即通過將數百萬個對象序列化為單個字節來交換更可預測的延遲[] - 呃!)
  • 不要在這部分中使用Java - 通過套接字與某種可預測的內存數據庫通信
  • 希望新的G1收藏家能夠提供幫助(主要適用於高流失案例)

只是花了很多時間在Java中使用巨型地圖的人的一些想法。


您可以嘗試使用內存數據庫,如HSQLDB

在我的情況下,我想創建一個包含2600萬條目的地圖。 使用標准Java HashMap,在2-3百萬次插入后,放置速率變得無法忍受。

從我的實驗(2009年的學生項目):

  • 我為100,000個節點建立了一個紅色黑樹,從1到100.000。 花了785.68秒(13分鍾)。 而且我沒有為100萬個節點構建RBTree(就像使用HashMap的結果一樣)。
  • 使用“Prime Tree”,我的算法數據結構。 我可以在21.29秒內為一千萬個節點建立樹/地圖(RAM:1.97Gb)。 搜索鍵值成本為O(1)。

注意:“Prime Tree”在“連續鍵”上的效果最好,從1到10百萬。 要使用像HashMap這樣的鍵,我們需要調整一些未成年人。


那么,什么是#PrimeTree? 簡而言之,它是像二叉樹一樣的樹數據結構,分支數是素數(而不是“2” - 二進制數)。

Effective Java:Programming Language Guide(Java Series)中

第3章,您可以在計算hashCode()時找到要遵循的良好規則。

特別:

如果該字段是數組,則將其視為每個元素都是單獨的字段。 也就是說,通過遞歸地應用這些規則來計算每個重要元素的哈希碼,並且每步驟2.b組合這些值。 如果數組字段中的每個元素都很重要,則可以使用版本1.5中添加的Arrays.hashCode方法之一。

您是否考慮過使用嵌入式數據庫來執行此操作。 看看Berkeley DB 它是開源的,現在由Oracle擁有。

它將所有內容存儲為Key-> Value對,它不是RDBMS。 它的目標是快速。

SQLite允許您在內存中使用它。

首先,您應該檢查您是否正確使用Map,鍵的好hashCode()方法,Map的初始容量,正確的Map實現等等許多其他答案描述。

然后我會建議使用分析器來查看實際發生的情況以及執行時間的花費。 例如,hashCode()方法執行了數十億次?

如果這沒有用,那么如何使用EHCachememcached之類的東西呢? 是的,它們是用於緩存的產品,但您可以對它們進行配置,以便它們具有足夠的容量,並且永遠不會從緩存存儲中逐出任何值。

另一種選擇是某些數據庫引擎,它比完整的SQL RDBMS重量更輕。 也許像Berkeley DB這樣的東西。

請注意,我個人沒有這些產品的性能經驗,但它們值得一試。

您可以嘗試將計算的哈希代碼緩存到密鑰對象。

像這樣的東西:

public int hashCode() {
  if(this.hashCode == null) {
     this.hashCode = computeHashCode();
  }
  return this.hashCode;
}

private int computeHashCode() {
   int hash = 503;
   hash = hash * 5381 + (a[0] + a[1]);
   hash = hash * 5381 + (b[0] + b[1] + b[2]);
   return hash;
}

當然,在第一次計算hashCode之后,您必須小心不要更改密鑰的內容。

編輯:當您將每個鍵只添加一次到地圖時,似乎緩存的代碼值是不值得的。 在其他一些情況下,這可能很有用。

另一張海報已經指出,由於您將值一起添加的方式,您的哈希碼實現將導致大量沖突。 我願意這樣做,如果你在調試器中查看HashMap對象,你會發現你有200個不同的哈希值,具有極長的存儲桶鏈。

如果總是具有0..51范圍內的值,則每個值將需要6位來表示。 如果您總是有5個值,則可以使用左移和添加創建一個30位哈希碼:

    int code = a[0];
    code = (code << 6) + a[1];
    code = (code << 6) + b[0];
    code = (code << 6) + b[1];
    code = (code << 6) + b[2];
    return code;

左移是快速的,但會留下不均勻分布的哈希碼(因為6位意味着范圍為0..63)。 另一種方法是將散列乘以51並添加每個值。 這仍然不會完美分布(例如,{2,0}和{1,52}將發生碰撞),並且將比移位慢。

    int code = a[0];
    code *= 51 + a[1];
    code *= 51 + b[0];
    code *= 51 + b[1];
    code *= 51 + b[2];
    return code;

正如所指出的,您的哈希碼實現有太多的沖突,修復它應該會產生不錯的性能。 此外,緩存hashCodes和有效實現equals將有所幫助。

如果您需要進一步優化:

根據您的描述,只有(52 * 51/2)*(52 * 51 * 50/6)= 29304600個不同的鍵(其中26000000,即約90%將存在)。 因此,您可以設計沒有任何沖突的哈希函數,並使用簡單數組而不是哈希映射來保存數據,從而減少內存消耗並提高查找速度:

T[] array = new T[Key.maxHashCode];

void put(Key k, T value) {
    array[k.hashCode()] = value;

T get(Key k) {
    return array[k.hashCode()];
}

(一般來說,設計一個有效的,無碰撞的散列函數是不可能很好地聚類,這就是為什么HashMap會容忍碰撞,這會產生一些開銷)

假設ab已排序,您可以使用以下哈希函數:

public int hashCode() {
    assert a[0] < a[1]; 
    int ahash = a[1] * a[1] / 2 
              + a[0];

    assert b[0] < b[1] && b[1] < b[2];

    int bhash = b[2] * b[2] * b[2] / 6
              + b[1] * b[1] / 2
              + b[0];
    return bhash * 52 * 52 / 2 + ahash;
}

static final int maxHashCode = 52 * 52 / 2 * 52 * 52 * 52 / 6;  

我認為這是無碰撞的。 證明這是留給數學傾向讀者的練習。

使用的流行散列方法對於大型集合並不是非常好,並且如上所述,使用的散列特別糟糕。 更好的是使用具有高混合和覆蓋的哈希算法,例如BuzHash(在http://www.java2s.com/Code/Java/Development-Class/AveryefficientjavahashalgorithmbasedontheBuzHashalgoritm.htm上的示例實現)

在開頭分配一張大地圖。 如果你知道它有2600萬個條目並且你有內存,那就做一個new HashMap(30000000)

你確定,你有足夠的內存用於2600萬個條目,擁有2600萬個密鑰和值嗎? 這對我來說聽起來像是很多記憶。 你確定垃圾收集在200到300萬的標記下還能正常嗎? 我可以想象這是一個瓶頸。

你可以嘗試兩件事:

  • 使您的 hashCode方法返回更簡單,更有效的內容,例如連續的int

  • 將地圖初始化為:

     
       
       
        
        Map map = new HashMap( 30000000, .95f );
       
        

這兩個動作將大大減少結構重復的數量,並且我認為很容易測試。

如果這不起作用,請考慮使用不同的存儲,例如RDBMS。

編輯

奇怪的是,設置初始容量會降低您的性能。

javadocs看

如果初始容量大於最大條目數除以加載因子,則不會發生重新加載操作。

我做了一個微觀標記(這不是任何意義上的確定,但至少證明了這一點)

 
 
 
  
  $cat Huge*java import java.util.*; public class Huge { public static void main( String [] args ) { Map map = new HashMap( 30000000 , 0.95f ); for( int i = 0 ; i < 26000000 ; i ++ ) { map.put( i, i ); } } } import java.util.*; public class Huge2 { public static void main( String [] args ) { Map map = new HashMap(); for( int i = 0 ; i < 26000000 ; i ++ ) { map.put( i, i ); } } } $time java -Xms2g -Xmx2g Huge real 0m16.207s user 0m14.761s sys 0m1.377s $time java -Xms2g -Xmx2g Huge2 real 0m21.781s user 0m20.045s sys 0m1.656s $
 
  

因此,由於重新使用,初始容量從21s降至16s。 這使我們將您的 hashCode方法作為“機會區域”;)

編輯

不是HashMap

按照你上一版的說法。

我認為你應該真正分析你的應用程序,看看內存/ CPU的使用位置。

我創建了一個實現相同hashCode的類

該哈希代碼會產生數百萬次沖突,然后HashMap中的條目會顯着減少。

我在之前的考試中從21s,16s傳到10s和8s。 原因是hashCode會引發大量的沖突,你不會存儲你認為的26M對象,但是數量要少得多(我會說約20k)所以:

問題不是HASHMAP是代碼中的其他地方。

現在是時候找到一個分析器並找出它的位置。 我認為這是在項目的創建或可能你正在寫入磁盤或從網絡接收數據。

這是我的課程實施。

注意我沒有像你那樣使用0-51范圍,但我的值沒有使用-126到127,並且重復承認,那是因為我在更新你的問題之前做了這個測試

唯一的區別是您的班級將有更多的碰撞,因此存儲在地圖中的項目更少。

 import java.util.*; public class Item { private static byte w = Byte.MIN_VALUE; private static byte x = Byte.MIN_VALUE; private static byte y = Byte.MIN_VALUE; private static byte z = Byte.MIN_VALUE; // Just to avoid typing :) private static final byte M = Byte.MAX_VALUE; private static final byte m = Byte.MIN_VALUE; private byte [] a = new byte[2]; private byte [] b = new byte[3]; public Item () { // make a different value for the bytes increment(); a[0] = z; a[1] = y; b[0] = x; b[1] = w; b[2] = z; } private static void increment() { z++; if( z == M ) { z = m; y++; } if( y == M ) { y = m; x++; } if( x == M ) { x = m; w++; } } public String toString() { return "" + this.hashCode(); } public int hashCode() { int hash = 503; hash = hash * 5381 + (a[0] + a[1]); hash = hash * 5381 + (b[0] + b[1] + b[2]); return hash; } // I don't realy care about this right now. public boolean equals( Object other ) { return this.hashCode() == other.hashCode(); } // print how many collisions do we have in 26M items. public static void main( String [] args ) { Set set = new HashSet(); int collisions = 0; for ( int i = 0 ; i < 26000000 ; i++ ) { if( ! set.add( new Item() ) ) { collisions++; } } System.out.println( collisions ); } } 

使用此類具有以前程序的Key

  map.put( new Item() , i ); 

給我:

 real 0m11.188s user 0m10.784s sys 0m0.261s real 0m9.348s user 0m9.071s sys 0m0.161s 

我用一個列表與一個hashmap做了一個小小的測試,有趣的是在列表中迭代,發現對象花費了相同的時間(以毫秒為單位),就像使用hashmaps獲取函數一樣...只是一個fyi。 哦,使用大小的哈希映射時,內存是一個大問題。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM