การเพิ่มประสิทธิภาพ / ทางเลือก Java HashMap


102

ฉันต้องการสร้าง HashMap ขนาดใหญ่ แต่put()ประสิทธิภาพไม่ดีพอ ความคิดใด ๆ ?

ยินดีรับคำแนะนำโครงสร้างข้อมูลอื่น ๆ แต่ฉันต้องการคุณสมบัติการค้นหาของ Java Map:

map.get(key)

ในกรณีของฉันฉันต้องการสร้างแผนที่ที่มี 26 ล้านรายการ การใช้ 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;
}

ฉันใช้คุณสมบัติการเชื่อมโยงของการเพิ่มเพื่อให้แน่ใจว่าวัตถุที่เท่ากันมีแฮชโค้ดเดียวกัน อาร์เรย์เป็นไบต์ที่มีค่าอยู่ในช่วง 0 - 51 ค่าจะถูกใช้เพียงครั้งเดียวในอาร์เรย์ใดอาร์เรย์ วัตถุจะเท่ากันถ้าอาร์เรย์มีค่าเดียวกัน (ในลำดับใดลำดับหนึ่ง) และเหมือนกันสำหรับอาร์เรย์ b ดังนั้น a = {0,1} b = {45,12,33} และ a = {1,0} b = {33,45,12} จึงเท่ากัน

แก้ไขหมายเหตุบางส่วน:

  • มีคนไม่กี่คนที่วิพากษ์วิจารณ์การใช้แผนที่แฮชหรือโครงสร้างข้อมูลอื่น ๆ เพื่อจัดเก็บข้อมูล 26 ล้านรายการ ฉันไม่เห็นว่าทำไมมันดูแปลก ๆ ดูเหมือนปัญหาโครงสร้างข้อมูลและอัลกอริทึมแบบคลาสสิกสำหรับฉัน ฉันมี 26 ล้านรายการและฉันต้องการที่จะสามารถแทรกและค้นหาได้อย่างรวดเร็วจากโครงสร้างข้อมูล: ให้โครงสร้างข้อมูลและอัลกอริทึม

  • การตั้งค่าความจุเริ่มต้นของ Java HashMap เริ่มต้นเป็น 26 ล้านจะลดประสิทธิภาพ

  • บางคนแนะนำให้ใช้ฐานข้อมูลในสถานการณ์อื่น ๆ ที่เป็นตัวเลือกที่ชาญฉลาดอย่างแน่นอน แต่ฉันกำลังถามคำถามเกี่ยวกับโครงสร้างข้อมูลและอัลกอริทึมจริงๆฐานข้อมูลแบบเต็มจะใช้งานมากเกินไปและช้ากว่าโซลูชันโครงสร้างข้อมูลที่ดีมาก (หลังจากที่ฐานข้อมูลทั้งหมดเป็นเพียงซอฟต์แวร์ แต่จะมีการสื่อสารและอาจมีค่าใช้จ่ายบนดิสก์)


29
หาก HashMap ทำงานช้าอาจเป็นไปได้ว่าฟังก์ชันแฮชของคุณไม่ดีพอ
Pascal Cuoq

12
หมอเจ็บมากตอนทำแบบนี้
skaffman

12
นี่เป็นคำถามที่ดีจริงๆ การสาธิตที่ดีว่าทำไมอัลกอริทึมการแฮชจึงมีความสำคัญและสิ่งที่ส่งผลต่อประสิทธิภาพที่อาจเกิดขึ้น
oxbow_lakes

12
ผลรวมของ a มีช่วง 0 ถึง 102 และผลรวมของ b มีช่วง 0 ถึง 153 ดังนั้นคุณจึงมีค่าแฮชที่เป็นไปได้เพียง 15,606 ค่าและค่าเฉลี่ย 1,666 คีย์ที่มี hashCode เดียวกัน คุณควรเปลี่ยน hashcode ของคุณเพื่อให้จำนวน hashCodes ที่เป็นไปได้นั้นมากกว่าจำนวนคีย์มาก
Peter Lawrey

6
ฉันได้พิจารณาแล้วว่าคุณกำลังสร้างโมเดล Texas Hold 'Em Poker ;-)
bacar

คำตอบ:


56

ตามที่หลายคนชี้ให้เห็นถึงhashCode()วิธีการคือการตำหนิ มันสร้างเพียง 20,000 รหัสสำหรับ 26 ล้านวัตถุที่แตกต่างกัน นั่นคือค่าเฉลี่ย 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

ดีขึ้นมาก. วิธีการแบบเก่าจะปิดลงอย่างรวดเร็วในขณะที่วิธีใหม่ช่วยให้มีปริมาณงานที่ดี


17
ฉันขอแนะนำว่าอย่าแก้ไขอาร์เรย์ในhashCodeวิธีการนี้ ตามแบบแผนhashCodeไม่เปลี่ยนสถานะของวัตถุ บางทีผู้สร้างอาจเป็นสถานที่ที่ดีกว่าในการจัดเรียง
Michael Myers

ฉันยอมรับว่าการเรียงลำดับของอาร์เรย์ควรเกิดขึ้นในตัวสร้าง รหัสที่แสดงดูเหมือนจะไม่ตั้งค่า hashCode การคำนวณโค้ดสามารถทำได้ง่ายขึ้นดังนี้: int result = a[0]; result = result * 52 + a[1]; //etc.
rsp

ฉันยอมรับว่าการเรียงลำดับในตัวสร้างแล้วคำนวณรหัสแฮชตามที่แนะนำ mmyers และ rsp นั้นดีกว่า ในกรณีของฉันวิธีแก้ปัญหาของฉันเป็นที่ยอมรับและฉันต้องการเน้นความจริงที่ว่าต้องเรียงลำดับอาร์เรย์เพื่อhashCode()ให้ทำงานได้
nash

3
โปรดทราบว่าคุณสามารถแคชแฮชโค้ดได้ (และทำให้ไม่ถูกต้องอย่างเหมาะสมหากอ็อบเจ็กต์ของคุณไม่แน่นอน)
NateS

1
ใช้เพียงแค่java.util.Arrays.hashCode () ง่ายกว่า (ไม่มีโค้ดให้เขียนและดูแลด้วยตัวเอง) การคำนวณอาจเร็วกว่า (การคูณน้อยลง) และการกระจายรหัสแฮชอาจจะมากขึ้น
jcsahnwaldt Reinstate Monica

18

สิ่งหนึ่งที่ฉันสังเกตเห็นในไฟล์ hashCode()วิธีการคือลำดับขององค์ประกอบในอาร์เรย์a[]และb[]ไม่สำคัญ ดังนั้นจะสับกับค่าเช่นเดียวกับ(a[]={1,2,3}, b[]={99,100}) (a[]={3,1,2}, b[]={100,99})จริงๆแล้วคีย์ทั้งหมดk1และk2ที่ไหน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]);

ที่ไหน c0 , c1และc3มีความแตกต่างกันคงที่ (คุณสามารถใช้ค่าคงที่แตกต่างกันสำหรับbถ้าจำเป็น) นั่นควรจะออกไปอีกสักหน่อย


แม้ว่าฉันควรเพิ่มว่ามันใช้ไม่ได้สำหรับฉันเพราะฉันต้องการคุณสมบัติที่อาร์เรย์ที่มีองค์ประกอบเดียวกันในคำสั่งซื้อที่แตกต่างกันให้แฮชโค้ดเดียวกัน
nash

5
ในกรณีนั้นคุณมีแฮชโค้ด 52C2 + 52C3 (23426 ตามเครื่องคิดเลขของฉัน) และแฮชแมปเป็นเครื่องมือที่ไม่ถูกต้องสำหรับงาน
kdgregory

จริงๆแล้วสิ่งนี้จะเพิ่มประสิทธิภาพ การชนกันมากขึ้น eq รายการน้อยลงใน eq แฮชแท็ก ทำงานน้อยลง ไม่ใช่แฮช (ซึ่งดูดี) หรือแฮชแท็ก (ซึ่งใช้งานได้ดี) ฉันพนันได้เลยว่ามันอยู่ที่การสร้างวัตถุที่ประสิทธิภาพลดลง
OscarRyz

7
@ ออสการ์ - การชนกันมากขึ้นเท่ากับว่าต้องทำมากขึ้นเพราะตอนนี้คุณต้องทำการค้นหาเชิงเส้นของแฮชเชน หากคุณมีค่าที่แตกต่างกัน 26,000,000 ค่าต่อเท่ากับ () และ 26,000 ค่าที่แตกต่างกันต่อ hashCode () โซ่ที่เก็บข้อมูลจะมีวัตถุ 1,000 รายการ
kdgregory

@ Nash0: ดูเหมือนคุณจะบอกว่าคุณต้องการให้สิ่งเหล่านี้มี hashCode เดียวกัน แต่ในขณะเดียวกันก็ไม่เท่ากัน (ตามที่กำหนดโดยวิธี equals ()) ทำไมคุณถึงต้องการเช่นนั้น?
MAK

17

เพื่ออธิบายรายละเอียดเกี่ยวกับ Pascal: คุณเข้าใจวิธีการทำงานของ HashMap หรือไม่? คุณมีสล็อตจำนวนหนึ่งในตารางแฮชของคุณ พบค่าแฮชสำหรับแต่ละคีย์จากนั้นแมปกับรายการในตาราง หากค่าแฮชสองค่าแมปกับรายการเดียวกัน - "แฮชชนกัน" - HashMap จะสร้างรายการที่เชื่อมโยง

การชนกันของแฮชสามารถฆ่าประสิทธิภาพของแผนที่แฮชได้ ในกรณีที่รุนแรงหากคีย์ทั้งหมดของคุณมีรหัสแฮชเหมือนกันหรือหากมีรหัสแฮชที่แตกต่างกัน แต่ทั้งหมดแมปไปยังสล็อตเดียวกันแผนที่แฮชของคุณจะกลายเป็นรายการที่เชื่อมโยงกัน

ดังนั้นหากคุณพบปัญหาด้านประสิทธิภาพสิ่งแรกที่ฉันต้องตรวจสอบคือ: ฉันได้รับการแจกแจงรหัสแฮชแบบสุ่มหรือไม่? ถ้าไม่คุณต้องมีฟังก์ชันแฮชที่ดีกว่านี้ ในกรณีนี้ "ดีกว่า" อาจหมายถึง "ดีกว่าสำหรับชุดข้อมูลเฉพาะของฉัน" เช่นสมมติว่าคุณกำลังทำงานกับสตริงและคุณเอาความยาวของสตริงเป็นค่าแฮช (ไม่ใช่วิธีการทำงานของ String.hashCode ของ Java แต่ฉันแค่สร้างตัวอย่างง่ายๆ) หากสตริงของคุณมีความยาวที่แตกต่างกันมากตั้งแต่ 1 ถึง 10,000 และมีการกระจายอย่างเท่าเทียมกันในช่วงนั้นซึ่งอาจเป็นสิ่งที่ดีมาก ฟังก์ชันแฮช แต่ถ้าสตริงของคุณเป็น 1 หรือ 2 อักขระทั้งหมดนี่จะเป็นฟังก์ชันแฮชที่ไม่ดี

แก้ไข: ฉันควรเพิ่ม: ทุกครั้งที่คุณเพิ่มรายการใหม่ HashMap จะตรวจสอบว่ารายการนี้ซ้ำกันหรือไม่ เมื่อมีการชนกันของแฮชจะต้องเปรียบเทียบคีย์ที่เข้ามากับทุกคีย์ที่แมปกับสล็อตนั้น ดังนั้นในกรณีที่เลวร้ายที่สุดที่ทุกอย่างแฮชเป็นช่องเดียวคีย์ที่สองจะถูกเปรียบเทียบกับคีย์แรกคีย์ที่สามจะเปรียบเทียบกับ # 1 และ # 2 คีย์ที่สี่จะถูกเปรียบเทียบกับ # 1, # 2 และ # 3 ฯลฯ เมื่อคุณขึ้นสู่คีย์ # 1 ล้านคุณได้ทำการเปรียบเทียบมากกว่าหนึ่งล้านล้านครั้ง

@Oscar: อืมฉันไม่เห็นว่ามัน "ไม่จริง" ได้อย่างไร มันเหมือนกับ "ให้ฉันชี้แจง" มากกว่า แต่ใช่มันเป็นเรื่องจริงที่ว่าหากคุณสร้างรายการใหม่ด้วยคีย์เดียวกับรายการที่มีอยู่สิ่งนี้จะเขียนทับรายการแรก นั่นคือสิ่งที่ฉันหมายถึงเมื่อฉันพูดถึงการค้นหารายการที่ซ้ำกันในย่อหน้าสุดท้าย: เมื่อใดก็ตามที่แฮชคีย์ไปยังช่องเดียวกัน HashMap จะต้องตรวจสอบว่ามันซ้ำกับคีย์ที่มีอยู่หรือไม่หรืออยู่ในช่องเดียวกันโดยบังเอิญของ ฟังก์ชันแฮช ฉันไม่รู้ว่านั่นคือ "จุดรวม" ของ HashMap: ฉันจะบอกว่า "จุดรวม" ก็คือคุณสามารถดึงองค์ประกอบด้วยคีย์ได้อย่างรวดเร็ว

แต่อย่างไรก็ตามนั่นไม่ได้ส่งผลกระทบต่อ "จุดรวม" ที่ฉันพยายามสร้าง: เมื่อคุณมีสองคีย์ใช่คีย์ที่แตกต่างกันไม่ใช่คีย์เดียวกันปรากฏขึ้นอีกครั้ง - แมปนั้นไปยังสล็อตเดียวกันในตาราง HashMap สร้างรายการที่เชื่อมโยง จากนั้นเนื่องจากต้องตรวจสอบคีย์ใหม่แต่ละคีย์เพื่อดูว่าแท้จริงแล้วซ้ำกับคีย์ที่มีอยู่หรือไม่แต่ละครั้งที่พยายามเพิ่มรายการใหม่ที่แมปไปยังสล็อตเดียวกันนี้จะต้องไล่ตามรายการที่เชื่อมโยงเพื่อตรวจสอบแต่ละรายการที่มีอยู่เพื่อดูว่านี่ ซ้ำกับคีย์ที่เห็นก่อนหน้านี้หรือเป็นคีย์ใหม่

อัปเดตยาวหลังจากโพสต์เดิม

ฉันเพิ่งได้รับการโหวตให้กับคำตอบนี้เมื่อ 6 ปีหลังจากโพสต์ซึ่งทำให้ฉันต้องอ่านคำถามอีกครั้ง

ฟังก์ชันแฮชที่ให้ในคำถามไม่ใช่แฮชที่ดีสำหรับ 26 ล้านรายการ

จะบวก [0] + a [1] และ b [0] + b [1] + b [2] เข้าด้วยกัน เขาบอกว่าค่าของแต่ละไบต์มีค่าตั้งแต่ 0 ถึง 51 ดังนั้นจึงให้เฉพาะ (51 * 2 + 1) * (51 * 3 + 1) = 15,862 ค่าแฮชที่เป็นไปได้ ด้วย 26 ล้านรายการซึ่งหมายถึงค่าเฉลี่ยประมาณ 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 ค่าที่กำหนดไว้ในคลาส

นอกจากนี้แบบร่างแรกที่ฟังก์ชันแฮชยังมีการคำนวณหลายอย่างที่ไม่ได้ทำอะไรเพื่อเพิ่มช่วงของเอาต์พุต สังเกตว่าเขาตั้งค่าแฮชเป็นครั้งแรก = 503 มากกว่าคูณด้วย 5381 ก่อนที่จะพิจารณาค่าจากคลาส ดังนั้น ... เขาจึงเพิ่ม 503 * 5381 ให้กับทุกค่า สิ่งนี้ทำให้สำเร็จ? การเพิ่มค่าคงที่ให้กับทุกค่าแฮชเพียงแค่เบิร์นวงจรซีพียูโดยไม่ทำอะไรให้เป็นประโยชน์ บทเรียนที่นี่: การเพิ่มความซับซ้อนให้กับฟังก์ชันแฮชไม่ใช่เป้าหมาย เป้าหมายคือการได้รับค่าต่างๆที่หลากหลายไม่ใช่แค่เพิ่มความซับซ้อนเพื่อประโยชน์ของความซับซ้อนเท่านั้น


3
ใช่ฟังก์ชันแฮชที่ไม่ถูกต้องจะส่งผลให้เกิดพฤติกรรมแบบนี้ +1
Henning

ไม่จริง รายการจะถูกสร้างขึ้นเฉพาะถ้ากัญชาจะเหมือนกัน แต่ที่สำคัญคือที่แตกต่างกัน ตัวอย่างเช่นถ้าให้แฮชโค้ด String 2345 และและจำนวนเต็มให้แฮชโค้ดเดียวกัน 2345 แล้วจำนวนเต็มถูกแทรกลงในรายการเพราะเป็นString.equals( Integer ) แต่ถ้าคุณมีคลาสเดียวกัน (หรืออย่างน้อยก็คืนค่าจริง) รายการเดียวกันจะถูกใช้ ตัวอย่างเช่นและ "new String (" one ") ที่ใช้เป็นคีย์จะใช้รายการเดียวกัน จริงๆแล้วนี่คือจุดทั้งหมดของ HashMap ตั้งแต่แรก! ดูตัวคุณเอง: pastebin.com/f20af40b9false.equalsnew String("one")
OscarRyz

3
@Oscar: ดูคำตอบของฉันต่อท้ายโพสต์เดิมของฉัน
เจย์

ฉันรู้ว่านี้เป็นหัวข้อเก่ามาก แต่นี่คือการอ้างอิงสำหรับคำว่า "ชน" ที่เกี่ยวข้องกับรหัสกัญชา: การเชื่อมโยง เมื่อคุณแทนที่ค่าในแฮชแมปโดยใส่ค่าอื่นด้วยคีย์เดียวกันจะไม่เรียกว่าการชนกัน
Tahir Akhtar

@ ตาเฮียร์เป๊ะ. บางทีโพสต์ของฉันอาจใช้คำไม่ดี ขอขอบคุณสำหรับการชี้แจง.
Jay

7

ความคิดแรกของฉันคือตรวจสอบให้แน่ใจว่าคุณกำลังเริ่มต้น HashMap อย่างเหมาะสม จากJavaDocs สำหรับ HashMap :

อินสแตนซ์ของ HashMap มีพารามิเตอร์สองตัวที่ส่งผลต่อประสิทธิภาพ: ความจุเริ่มต้นและปัจจัยการโหลด ความจุคือจำนวนที่เก็บข้อมูลในตารางแฮชและความจุเริ่มต้นเป็นเพียงความจุ ณ เวลาที่สร้างตารางแฮช โหลดแฟคเตอร์คือการวัดว่าตารางแฮชเต็มจำนวนเท่าใดก่อนที่ความจุจะเพิ่มขึ้นโดยอัตโนมัติ เมื่อจำนวนรายการในตารางแฮชเกินผลคูณของโหลดแฟคเตอร์และความจุปัจจุบันตารางแฮชจะถูกสร้างใหม่ (นั่นคือโครงสร้างข้อมูลภายในจะถูกสร้างขึ้นใหม่) เพื่อให้ตารางแฮชมีจำนวนที่เก็บข้อมูลประมาณสองเท่า

ดังนั้นหากคุณเริ่มต้นด้วย HashMap ที่เล็กเกินไปทุกครั้งที่จำเป็นต้องปรับขนาดแฮชทั้งหมดจะถูกคำนวณใหม่ ... ซึ่งอาจเป็นสิ่งที่คุณรู้สึกเมื่อไปถึงจุดแทรก 2-3 ล้านจุด


ฉันไม่คิดว่าจะมีการคำนวณซ้ำ ขนาดโต๊ะเพิ่มขึ้นแฮชจะถูกเก็บไว้
Henning

Hashmap ทำเพียงเล็กน้อยและสำหรับทุกรายการ: newIndex = storesHash & newLength;
Henning

4
Hanning: บางทีถ้อยคำที่ไม่ดีในส่วนของ delfuego แต่ประเด็นนั้นถูกต้อง ใช่ค่าแฮชจะไม่ถูกคำนวณใหม่ในแง่ที่ว่าเอาต์พุตของ hashCode () ไม่ได้ถูกคำนวณใหม่ แต่เมื่อขนาดตารางเพิ่มขึ้นจะต้องใส่คีย์ทั้งหมดลงในตารางอีกครั้งนั่นคือค่าแฮชจะต้องแฮชใหม่เพื่อให้ได้หมายเลขสล็อตใหม่ในตาราง
เจย์

เจย์ใช่ - ถ้อยคำที่ไม่ดีจริง ๆ และสิ่งที่คุณพูด :)
delfuego

1
@delfuego และ @ nash0: Yeap การตั้งค่าความจุเริ่มต้นเท่ากับจำนวนองค์ประกอบจะทำให้ประสิทธิภาพลดลงเนื่องจากคุณมีการชนกันนับล้านครั้งดังนั้นคุณจึงใช้ความจุเพียงเล็กน้อยเท่านั้น แม้ว่าคุณจะใช้รายการที่มีอยู่ทั้งหมด แต่การตั้งค่าความจุเท่ากันจะทำให้แย่ที่สุด! เนื่องจากปัจจัยการโหลดจะมีการร้องขอพื้นที่มากขึ้น คุณจะต้องใช้initialcapactity = maxentries/loadcapacity(เช่น 30M, 0.95 สำหรับ 26M รายการ) แต่นี่ไม่ใช่กรณีของคุณเนื่องจากคุณมีการชนทั้งหมดที่คุณใช้เพียงไม่เกิน 20k
OscarRyz

7

ฉันขอแนะนำแนวทางสามง่าม:

  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 ที่คุณมีอยู่แม้ว่าจะขึ้นอยู่กับลักษณะของข้อมูลของคุณก็ตาม


RAM อาจมีขนาดเล็กสำหรับแผนที่และอาร์เรย์ประเภทนี้ดังนั้นฉันจึงสงสัยว่ามีปัญหาข้อ จำกัด ของหน่วยความจำ
ReneS

7

การเข้าสู่พื้นที่สีเทาของ "เปิด / ปิดหัวข้อ" แต่จำเป็นต้องขจัดความสับสนเกี่ยวกับคำแนะนำของ Oscar Reyes ว่าการชนกันของแฮชมากขึ้นเป็นสิ่งที่ดีเพราะจะช่วยลดจำนวนองค์ประกอบใน HashMap ฉันอาจเข้าใจผิดในสิ่งที่ออสการ์พูด แต่ดูเหมือนฉันจะไม่ใช่คนเดียว: kdgregory, delfuego, Nash0 และดูเหมือนว่าฉันทุกคนจะมีความเข้าใจ (ผิด ๆ ) เหมือนกัน

ถ้าฉันเข้าใจสิ่งที่ออสการ์พูดเกี่ยวกับคลาสเดียวกันที่มีแฮชโค้ดเดียวกันเขาเสนอว่าจะมีการแทรกอินสแตนซ์ของคลาสเดียวที่มีแฮชโค้ดที่กำหนดไว้ใน HashMap เช่นถ้าฉันมีอินสแตนซ์ของ SomeClass ที่มีแฮชโค้ดเป็น 1 และอินสแตนซ์ที่สองของ SomeClass ที่มีแฮชโค้ดเป็น 1 จะมีการแทรกอินสแตนซ์ของ SomeClass เพียงอินสแตนซ์เดียว

ตัวอย่าง Java pastebin ที่http://pastebin.com/f20af40b9ดูเหมือนจะระบุว่าข้างต้นสรุปสิ่งที่ออสการ์เสนอได้อย่างถูกต้อง

ไม่ว่าความเข้าใจหรือความเข้าใจผิดใด ๆ สิ่งที่เกิดขึ้นคือกรณีที่แตกต่างกันของคลาสเดียวกันไม่ได้ได้รับการแทรกเพียงครั้งเดียวเข้า HashMap ถ้าพวกเขามีแฮชโค้ดเดียวกัน - ไม่ได้จนกว่าจะมีการกำหนดว่ากุญแจที่มีค่าเท่ากันหรือไม่ สัญญาแฮชโค้ดกำหนดให้อ็อบเจ็กต์ที่เท่ากันมีแฮชโค้ดเดียวกัน อย่างไรก็ตามไม่ต้องการให้วัตถุที่ไม่เท่ากันมีรหัสแฮชที่แตกต่างกัน (แม้ว่าสิ่งนี้อาจเป็นที่ต้องการด้วยเหตุผลอื่นก็ตาม) [1]

ตัวอย่าง pastebin.com/f20af40b9 (ซึ่ง Oscar อ้างถึงอย่างน้อยสองครั้ง) ตามหลัง แต่ปรับเปลี่ยนเล็กน้อยเพื่อใช้การยืนยัน JUnit แทนการพิมพ์ ตัวอย่างนี้ใช้เพื่อสนับสนุนข้อเสนอที่ว่ารหัสแฮชเดียวกันทำให้เกิดการชนกันและเมื่อคลาสเหมือนกันจะมีการสร้างรายการเดียวเท่านั้น (เช่นสตริงเดียวในกรณีเฉพาะนี้):

@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 ละเลยคือความจริงที่ว่าทั้งคู่sและeseเท่ากัน: ทั้งคู่เป็นสตริง "ese" ดังนั้นการแทรกหรือรับเนื้อหาของแผนที่โดยใช้sหรือeseหรือ"ese"เป็นคีย์จึงเทียบเท่ากันทั้งหมดเนื่องจากs.equals(ese) && s.equals("ese")เป็นกุญแจสำคัญที่มีทั้งหมดเพราะเทียบเท่า

การทดสอบครั้งที่สองแสดงให้เห็นว่าผิดพลาดที่จะสรุปว่าแฮชโค้ดที่เหมือนกันในคลาสเดียวกันเป็นสาเหตุที่ทำให้คีย์ -> ค่าs -> 1ถูกเขียนทับese -> 2เมื่อmap.put(ese, 2)ถูกเรียกในการทดสอบหนึ่ง ในการทดสอบที่สองsและeseยังคงมีแฮชโค้ดเดียวกัน (ตรวจสอบโดยassertEquals(s.hashCode(), ese.hashCode());) และเป็นคลาสเดียวกัน อย่างไรก็ตามsและeseเป็นMyStringอินสแตนซ์ในการทดสอบนี้ไม่ใช่Stringอินสแตนซ์Java โดยความแตกต่างเดียวที่เกี่ยวข้องกับการทดสอบนี้คือค่าเท่ากับString s equals String 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;
    }
}

จากความคิดเห็นในภายหลังออสการ์ดูเหมือนจะย้อนกลับสิ่งที่เขาพูดก่อนหน้านี้และยอมรับความสำคัญของความเท่าเทียม อย่างไรก็ตามดูเหมือนว่าความคิดที่เท่าเทียมกันคือสิ่งที่สำคัญไม่ใช่ "คลาสเดียวกัน" ยังไม่ชัดเจน (เน้นของฉัน):

"ไม่จริงรายการจะถูกสร้างขึ้นก็ต่อเมื่อแฮชเหมือนกัน แต่คีย์แตกต่างกันตัวอย่างเช่นหากสตริงให้แฮชโค้ด 2345 และและจำนวนเต็มให้แฮชโค้ด 2345 เดียวกันดังนั้นจำนวนเต็มจะถูกแทรกลงในรายการเนื่องจากสตริง เท่ากับ (จำนวนเต็ม) เป็นเท็จ แต่ถ้าคุณมีคลาสเดียวกัน (หรืออย่างน้อย .equals ส่งกลับค่าจริง)จะใช้รายการเดียวกันตัวอย่างเช่นสตริงใหม่ ("หนึ่ง") และ "สตริงใหม่ (" หนึ่ง ") ที่ใช้เป็น จะใช้รายการเดียวกันอันที่จริงนี่คือจุดทั้งหมดของ HashMap ตั้งแต่แรก! ดูตัวเอง: pastebin.com/f20af40b9 - Oscar Reyes "

เมื่อเทียบกับความคิดเห็นก่อนหน้านี้ที่กล่าวถึงความสำคัญของคลาสที่เหมือนกันและแฮชโค้ดเดียวกันอย่างชัดเจนโดยไม่มีการกล่าวถึงความเท่าเทียมกัน:

"@delfuego: ดูตัวเอง: pastebin.com/f20af40b9 ดังนั้นในคำถามนี้มีการใช้คลาสเดียวกัน (รอสักครู่คลาสเดียวกันจะถูกใช้ใช่ไหม) ซึ่งหมายความว่าเมื่อแฮชเดียวกันถูกใช้รายการเดียวกัน ถูกใช้และไม่มี "รายชื่อ" ของรายการ - Oscar Reyes "

หรือ

"อันที่จริงสิ่งนี้จะเพิ่มประสิทธิภาพยิ่งมีการชนกันมากขึ้น eq รายการน้อยลงในแฮชแท็ก eq งานที่ต้องทำน้อยลงไม่ใช่แฮช (ซึ่งดูดี) หรือแฮชแท็ก (ซึ่งใช้งานได้ดี) ฉันพนันได้เลยว่ามันอยู่ที่วัตถุ การสร้างที่การแสดงเสื่อมเสีย - Oscar Reyes "

หรือ

"@kdgregory: ใช่ แต่เฉพาะในกรณีที่การชนกันเกิดขึ้นกับคลาสที่แตกต่างกันสำหรับคลาสเดียวกัน (ซึ่งเป็นกรณีนี้) จะใช้รายการเดียวกัน - Oscar Reyes"

อีกครั้งฉันอาจเข้าใจผิดในสิ่งที่ออสการ์พยายามจะพูดจริงๆ อย่างไรก็ตามความคิดเห็นเดิมของเขาทำให้เกิดความสับสนมากพอที่ดูเหมือนว่าจะรอบคอบที่จะเคลียร์ทุกอย่างด้วยการทดสอบที่ชัดเจนดังนั้นจึงไม่มีข้อสงสัยที่ค้างคา


[1] - จากภาษา Java ที่มีประสิทธิภาพฉบับที่สองโดย Joshua Bloch:

  • เมื่อใดก็ตามที่เรียกใช้บนอ็อบเจ็กต์เดียวกันมากกว่าหนึ่งครั้งในระหว่างการเรียกใช้แอ็พพลิเคชันเมธอด hashCode จะต้องส่งคืนจำนวนเต็มเดียวกันอย่างสม่ำเสมอโดยจะไม่มีการแก้ไขข้อมูลที่ใช้ในการเปรียบเทียบ s ที่เท่ากันบนอ็อบเจ็กต์ จำนวนเต็มนี้ไม่จำเป็นต้องคงที่จากการทำงานของแอปพลิเคชันหนึ่งไปจนถึงการดำเนินการอื่น ๆ

  • ถ้าวัตถุสองชิ้นมีค่าเท่ากันตามวิธี s (Obj ect) เท่ากันการเรียกใช้เมธอด hashCode บนวัตถุทั้งสองจะต้องให้ผลลัพธ์จำนวนเต็มเท่ากัน

  • ไม่จำเป็นว่าถ้าสองอ็อบเจกต์ไม่เท่ากันตามเมธอด s (Object) เท่ากันดังนั้นการเรียกเมธอด hashCode บนอ็อบเจ็กต์ทั้งสองแต่ละอ็อบเจ็กต์จะต้องให้ผลลัพธ์จำนวนเต็มที่แตกต่างกัน อย่างไรก็ตามโปรแกรมเมอร์ควรทราบว่าการสร้างผลลัพธ์จำนวนเต็มที่แตกต่างกันสำหรับอ็อบเจ็กต์ที่ไม่เท่ากันอาจปรับปรุงประสิทธิภาพของตารางแฮช


5

หากอาร์เรย์ใน hashCode ที่โพสต์ของคุณเป็นไบต์คุณอาจจะมีรายการซ้ำกันจำนวนมาก

a [0] + a [1] จะอยู่ระหว่าง 0 ถึง 512 เสมอการเพิ่ม b จะทำให้ได้ตัวเลขระหว่าง 0 ถึง 768 เสมอคูณค่าเหล่านั้นและคุณจะได้ชุดค่าผสมที่ไม่ซ้ำกันสูงสุด 400,000 ชุดโดยสมมติว่าข้อมูลของคุณกระจายอย่างสมบูรณ์ ในทุกค่าที่เป็นไปได้ของแต่ละไบต์ หากข้อมูลของคุณเป็นปกติคุณอาจมีผลลัพธ์ที่ไม่ซ้ำกันน้อยกว่ามาก


4

HashMap มีความจุเริ่มต้นและประสิทธิภาพของ HashMap นั้นขึ้นอยู่กับ hashCode ที่สร้างวัตถุต้นแบบ

ลองปรับแต่งทั้งสองอย่าง


4

หากคีย์มีรูปแบบใด ๆ คุณสามารถแบ่งแผนที่ออกเป็นแผนที่ขนาดเล็กและมีแผนที่ดัชนี

ตัวอย่าง: คีย์: 1,2,3, .... n 28 แผนที่ละ 1 ล้าน แผนที่ดัชนี: 1-1,000,000 -> แผนที่ 1 1,000,000-2,000,000 -> แผนที่ 2

ดังนั้นคุณจะทำการค้นหาสองครั้ง แต่ชุดคีย์จะเป็น 1,000,000 เทียบกับ 28,000,000 คุณสามารถทำได้อย่างง่ายดายด้วยรูปแบบการต่อย

หากคีย์เป็นแบบสุ่มอย่างสมบูรณ์สิ่งนี้จะไม่ทำงาน


1
แม้ว่าคีย์จะเป็นแบบสุ่มคุณสามารถใช้ (key.hashCode ()% 28) เพื่อเลือกแผนที่ที่จะจัดเก็บคีย์ - ค่านั้นได้
Juha Syrjälä

4

ถ้าอาร์เรย์สองไบต์ที่คุณพูดถึงเป็นคีย์ทั้งหมดของคุณค่าจะอยู่ในช่วง 0-51 ไม่ซ้ำกันและลำดับภายในอาร์เรย์ a และ b ไม่มีนัยสำคัญคณิตศาสตร์ของฉันบอกฉันว่ามีการเรียงสับเปลี่ยนที่เป็นไปได้เพียงประมาณ 26 ล้านรายการและ ที่คุณน่าจะพยายามเติมเต็มแผนที่ด้วยค่าสำหรับคีย์ที่เป็นไปได้ทั้งหมด

ในกรณีนี้ทั้งการกรอกและการดึงค่าจากที่เก็บข้อมูลของคุณจะเร็วกว่ามากหากคุณใช้อาร์เรย์แทน HashMap และจัดทำดัชนีตั้งแต่ 0 ถึง 25989599


นั่นเป็นความคิดที่ดีมากและอันที่จริงฉันกำลังทำเช่นนั้นสำหรับปัญหาการจัดเก็บข้อมูลอื่นที่มีองค์ประกอบ 1.2 พันล้านรายการ ในกรณีนี้ฉันต้องการหาวิธีง่ายๆและใช้โครงสร้างข้อมูลที่สร้างไว้ล่วงหน้า :)
nash

4

ฉันมาสาย แต่มีความคิดเห็นสองสามข้อเกี่ยวกับแผนที่ขนาดใหญ่:

  1. ตามที่กล่าวไว้ในบทความอื่น ๆ ด้วย hashCode () ที่ดี 26M รายการในแผนที่ไม่ใช่เรื่องใหญ่
  2. อย่างไรก็ตามปัญหาที่อาจซ่อนอยู่ที่นี่คือผลกระทบ GC ของแผนที่ยักษ์

ฉันตั้งสมมติฐานว่าแผนที่เหล่านี้มีอายุยืนยาว กล่าวคือคุณเติมข้อมูลพวกเขาและพวกเขาติดอยู่ตลอดระยะเวลาของแอพ ฉันยังสมมติว่าแอพนั้นมีอายุการใช้งานยาวนาน - เหมือนเซิร์ฟเวอร์บางประเภท

แต่ละรายการใน Java HashMap ต้องการอ็อบเจ็กต์สามอย่างคือคีย์ค่าและรายการที่เชื่อมโยงเข้าด้วยกัน ดังนั้น 26M รายการในแผนที่หมายถึง 26M * 3 == 78M วัตถุ ซึ่งใช้ได้จนกว่าคุณจะเข้าสู่ GC เต็มรูปแบบ จากนั้นคุณมีปัญหาหยุดโลก GC จะดูวัตถุ 78M แต่ละชิ้นและพิจารณาว่าพวกมันมีชีวิตทั้งหมด วัตถุ 78M + เป็นเพียงวัตถุจำนวนมากที่ต้องมอง หากแอปของคุณสามารถทนต่อการหยุดชั่วคราวเป็นเวลานาน (อาจจะหลายวินาที) ก็ไม่มีปัญหา หากคุณพยายามที่จะบรรลุความล่าช้าใด ๆ รับประกันได้ว่าคุณอาจมีปัญหาสำคัญ (แน่นอนว่าหากคุณต้องการการรับประกันเวลาแฝง Java ไม่ใช่แพลตฟอร์มที่จะเลือก :)) หากค่าในแผนที่ของคุณหมุนเร็วคุณจะพบกับการรวบรวมแบบเต็มบ่อยครั้ง ซึ่งทำให้เกิดปัญหาขึ้นอย่างมาก

ฉันไม่รู้วิธีแก้ปัญหาที่ยอดเยี่ยมสำหรับปัญหานี้ แนวคิด:

  • บางครั้งอาจเป็นไปได้ที่จะปรับแต่ง GC และขนาดฮีปเป็น "ส่วนใหญ่" เพื่อป้องกัน GC แบบเต็ม
  • หากเนื้อหาแผนที่ของคุณปั่นป่วนมากคุณสามารถลองFastMap ของ Javolution - มันสามารถรวมวัตถุเข้าซึ่งอาจลดความถี่ในการรวบรวมทั้งหมด
  • คุณสามารถสร้างแผนที่ของคุณเองได้และทำการจัดการหน่วยความจำอย่างชัดเจนบนไบต์ [] (เช่นการแลกเปลี่ยนซีพียูสำหรับเวลาแฝงที่คาดเดาได้มากขึ้นโดยการจัดลำดับวัตถุนับล้านให้เป็นไบต์เดียว [] - ฮึ!)
  • อย่าใช้ Java สำหรับส่วนนี้ - พูดคุยกับ DB ในหน่วยความจำที่คาดเดาได้ผ่านซ็อกเก็ต
  • หวังว่าคอลเลกชันG1ใหม่จะช่วยได้ (ส่วนใหญ่ใช้กับกรณีการปั่นไฟสูง)

เพียงแค่ความคิดบางอย่างจากคนที่ใช้เวลากับแผนที่ขนาดยักษ์ใน Java



3

ในกรณีของฉันฉันต้องการสร้างแผนที่ที่มี 26 ล้านรายการ การใช้ Java HashMap มาตรฐานอัตราการใส่จะช้าลงเหลือทนหลังจากการแทรก 2-3 ล้านครั้ง

จากการทดลองของฉัน (โครงการนักเรียนในปี 2552):

  • ฉันสร้าง Red Black Tree สำหรับ 100.000 โหนดจาก 1 ถึง 100.000 ใช้เวลา 785.68 วินาที (13 นาที) และฉันไม่สามารถสร้าง RBTree สำหรับ 1 ล้านโหนด (เช่นผลลัพธ์ของคุณด้วย HashMap)
  • การใช้ "Prime Tree" โครงสร้างข้อมูลอัลกอริทึมของฉัน ฉันสามารถสร้างต้นไม้ / แผนที่สำหรับ 10 ล้านโหนดภายใน 21.29 วินาที (RAM: 1.97Gb) ต้นทุนคีย์ - ค่าการค้นหาคือ O (1)

หมายเหตุ: "Prime Tree" ทำงานได้ดีที่สุดกับ "คีย์ต่อเนื่อง" ตั้งแต่ 1 - 10 ล้าน ในการทำงานกับคีย์เช่น HashMap เราจำเป็นต้องมีการปรับผู้เยาว์


แล้ว #PrimeTree คืออะไร? กล่าวโดยย่อก็คือโครงสร้างข้อมูลแบบทรีเช่น Binary Tree โดยหมายเลขสาขาเป็นจำนวนเฉพาะ (แทนที่จะเป็น "2" - ไบนารี)


คุณช่วยแชร์ลิงค์หรือการใช้งานได้ไหม
เบญจ



1

คุณพิจารณาใช้ฐานข้อมูลแบบฝังเพื่อทำสิ่งนี้หรือไม่ ดูBerkeley DB ตอนนี้เป็นโอเพนซอร์สซึ่งเป็นของ Oracle

จัดเก็บทุกอย่างเป็นคู่คีย์ -> ค่าไม่ใช่ RDBMS และมีเป้าหมายที่จะรวดเร็ว


2
Berkeley DB ไม่มีที่ไหนใกล้เร็วพอสำหรับจำนวนรายการนี้เนื่องจากค่าใช้จ่ายในการทำให้เป็นอนุกรม / IO มันไม่เคยเร็วไปกว่าแฮชแมปและ OP ไม่สนใจเรื่องการคงอยู่ ข้อเสนอแนะของคุณไม่ดี
oxbow_lakes

1

ก่อนอื่นคุณควรตรวจสอบว่าคุณใช้แผนที่อย่างถูกต้องวิธีการ hashCode () ที่ดีสำหรับคีย์ความจุเริ่มต้นสำหรับแผนที่การใช้งานแผนที่ที่ถูกต้อง ฯลฯ เช่นเดียวกับคำตอบอื่น ๆ ที่อธิบายไว้

จากนั้นฉันขอแนะนำให้ใช้ profiler เพื่อดูว่าเกิดอะไรขึ้นจริงและใช้เวลาดำเนินการที่ใด ตัวอย่างเช่น hashCode () method ดำเนินการเป็นพันล้านครั้งหรือไม่?

หากไม่ได้ผลจะใช้EHCacheหรือmemcachedอย่างไร ใช่เป็นผลิตภัณฑ์สำหรับการแคช แต่คุณสามารถกำหนดค่าเพื่อให้มีความจุเพียงพอและจะไม่นำค่าใด ๆ ออกจากที่เก็บแคช

อีกทางเลือกหนึ่งคือเครื่องมือฐานข้อมูลบางตัวที่มีน้ำหนักเบากว่า SQL RDBMS แบบเต็ม บางอย่างเช่นBerkeley DBอาจจะ

โปรดทราบว่าโดยส่วนตัวแล้วฉันไม่มีประสบการณ์เกี่ยวกับประสิทธิภาพของผลิตภัณฑ์เหล่านี้ แต่ก็คุ้มค่าที่จะลอง


1

คุณสามารถลองแคชโค้ดแฮชที่คำนวณแล้วไปยังคีย์ออบเจ็กต์

สิ่งนี้:

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 ซ้ำเมื่อมีการปรับขนาดดังนั้นจึงไม่ทำให้คุณได้รับอะไรเลย
delfuego

1

ผู้โพสต์คนอื่นได้ชี้ให้เห็นแล้วว่าการใช้แฮชโค้ดของคุณจะทำให้เกิดการชนกันมากมายเนื่องจากวิธีที่คุณเพิ่มค่าเข้าด้วยกัน ฉันเต็มใจที่จะเป็นอย่างนั้นถ้าคุณดูที่วัตถุ 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;

@kdgregory: ฉันตอบไปแล้วว่า "การชนกันมากขึ้นหมายถึงการทำงานมากขึ้น" ที่อื่น :)
OscarRyz

1

ตามที่ระบุไว้การใช้งานแฮชโค้ดของคุณมีการชนกันมากเกินไปและการแก้ไขควรส่งผลให้มีประสิทธิภาพที่ดี ยิ่งไปกว่านั้นการแคช hashCodes และการนำไปใช้อย่างมีประสิทธิภาพจะช่วยได้

หากคุณต้องการเพิ่มประสิทธิภาพให้ดียิ่งขึ้น:

ตามคำอธิบายของคุณมีเพียง (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 สามารถทนต่อการชนกันซึ่งมีค่าใช้จ่ายบางส่วน)

สมมติว่าaและbจะเรียงคุณอาจใช้ฟังก์ชั่นกัญชาต่อไปนี้:

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;  

ฉันคิดว่านี่ไม่มีการชนกัน การพิสูจน์สิ่งนี้ถือเป็นแบบฝึกหัดสำหรับผู้อ่านที่มีความโน้มเอียงทางคณิตศาสตร์


1

ในJava ที่มีประสิทธิภาพ: คู่มือภาษาการเขียนโปรแกรม (ซีรี่ส์ Java)

บทที่ 3 คุณสามารถค้นหากฎที่ดีที่จะปฏิบัติตามเมื่อคำนวณ hashCode ()

พิเศษ:

หากฟิลด์นั้นเป็นอาร์เรย์ให้ปฏิบัติราวกับว่าแต่ละองค์ประกอบเป็นฟิลด์แยกกัน นั่นคือคำนวณรหัสแฮชสำหรับแต่ละองค์ประกอบที่มีนัยสำคัญโดยใช้กฎเหล่านี้แบบวนซ้ำและรวมค่าเหล่านี้ตามขั้นตอนที่ 2 b. หากทุกองค์ประกอบในฟิลด์อาร์เรย์มีความสำคัญคุณสามารถใช้หนึ่งในวิธี Arrays.hashCode ที่เพิ่มในรีลีส 1.5


0

จัดสรรแผนที่ขนาดใหญ่ในการเริ่มต้น ถ้าคุณรู้ว่ามันจะมี 26 ล้านรายการและคุณมีหน่วยความจำสำหรับมันให้ทำnew HashMap(30000000).

แน่ใจหรือว่าคุณมีหน่วยความจำเพียงพอสำหรับ 26 ล้านรายการที่มีคีย์และค่า 26 ล้านคีย์ ฟังดูเหมือนเป็นความทรงจำสำหรับฉันมาก คุณแน่ใจหรือไม่ว่าการจัดเก็บขยะยังคงทำได้ดีอยู่ที่ 2 ถึง 3 ล้านเครื่องหมายของคุณ ฉันนึกภาพออกว่าเป็นคอขวด


2
อ้ออีกอย่าง รหัสแฮชของคุณจะต้องกระจายอย่างเท่าเทียมกันเพื่อหลีกเลี่ยงรายการที่เชื่อมโยงจำนวนมากในตำแหน่งเดียวในแผนที่
ReneS

0

คุณสามารถลองสองสิ่ง:

  • ทำให้hashCodeวิธีการของคุณ ส่งคืนสิ่งที่ง่ายและมีประสิทธิภาพมากขึ้นเช่น int ติดต่อกัน

  • เริ่มต้นแผนที่ของคุณเป็น:

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

การกระทำทั้งสองนี้จะช่วยลดจำนวนการปรับโครงสร้างใหม่ได้อย่างมากและฉันคิดว่าค่อนข้างง่ายที่จะทดสอบ

หากไม่ได้ผลให้พิจารณาใช้ที่เก็บข้อมูลอื่นเช่น RDBMS

แก้ไข

เป็นเรื่องแปลกที่การตั้งค่าความจุเริ่มต้นลดประสิทธิภาพในกรณีของคุณ

ดูจากJavadocs :

หากความจุเริ่มต้นมากกว่าจำนวนสูงสุดของรายการหารด้วยปัจจัยการโหลดจะไม่มีการดำเนินการ rehash เกิดขึ้น

ฉันทำไมโครบีชมาร์ก (ซึ่งไม่ได้หมายถึงขั้นสุดท้าย แต่อย่างน้อยก็พิสูจน์จุดนี้ได้)

$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
$

ดังนั้นการใช้กำลังการผลิตเริ่มต้นจะลดลงจาก 21 วินาทีเป็น 16 วินาทีเนื่องจากการเปลี่ยนใหม่ ที่ปล่อยให้เราใช้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

3
ออสการ์ตามที่ระบุไว้ในที่อื่น ๆ ข้างต้น (ในการตอบกลับความคิดเห็นของคุณ) ดูเหมือนว่าคุณจะคิดว่าการชนกันมากกว่านั้นเป็นสิ่งที่ดี มันไม่ดีมาก การชนกันหมายความว่าสล็อตที่แฮชที่กำหนดจะเปลี่ยนจากการมีรายการเดียวไปสู่การมีรายการรายการและรายการนี้จะต้องถูกค้นหา / ข้ามผ่านทุกครั้งที่เข้าถึงสล็อต
delfuego

@delfuego: ไม่ได้เกิดขึ้นเฉพาะเมื่อคุณมีการชนกันโดยใช้คลาสที่แตกต่างกัน แต่สำหรับคลาสเดียวกันจะใช้รายการเดียวกัน)
OscarRyz

2
@Oscar - ดูคำตอบของฉันกับคุณด้วยคำตอบของ MAK HashMap เก็บรักษารายการที่เชื่อมโยงกันในที่เก็บแฮชแต่ละรายการและดำเนินการเรียกรายการนั้นเท่ากับ () ในทุกองค์ประกอบ คลาสของออบเจ็กต์ไม่มีส่วนเกี่ยวข้องกับมัน (นอกเหนือจากการลัดวงจรที่เท่ากับ ())
kdgregory

1
@Oscar - การอ่านคำตอบของคุณดูเหมือนว่าคุณสมมติว่า equals () จะกลับมาเป็นจริงหากรหัสแฮชเหมือนกัน นี่ไม่ใช่ส่วนหนึ่งของสัญญา equals / hashcode หากฉันเข้าใจผิดอย่าสนใจความคิดเห็นนี้
kdgregory

1
ขอบคุณมากสำหรับความพยายามของออสการ์ แต่ฉันคิดว่าคุณกำลังสับสนว่าออบเจ็กต์สำคัญเท่ากันกับการมีรหัสแฮชเดียวกัน นอกจากนี้ในลิงก์โค้ดรายการหนึ่งของคุณที่คุณใช้สตริงเท่ากับเป็นคีย์โปรดจำไว้ว่าสตริงใน Java ไม่เปลี่ยนรูป ฉันคิดว่าเราทั้งคู่ได้เรียนรู้มากมายเกี่ยวกับการแฮชในวันนี้ :)
nash


0

ฉันทำการทดสอบเล็กน้อยในขณะที่กลับมาพร้อมกับรายการเทียบกับแฮชแมปสิ่งที่ตลกคือการวนซ้ำผ่านรายการและการค้นหาวัตถุนั้นใช้เวลาในหน่วยมิลลิวินาทีเท่ากันกับการใช้ฟังก์ชันรับแฮชแมป ... เพียงแค่ fyi โอ้ใช่หน่วยความจำเป็นปัญหาใหญ่เมื่อทำงานกับแฮชแมปขนาดนั้น


0

วิธีการแฮชที่นิยมใช้นั้นไม่ค่อยดีนักสำหรับชุดใหญ่และดังที่ระบุไว้ข้างต้นแฮชที่ใช้นั้นไม่ดีอย่างยิ่ง ดีกว่าคือการใช้อัลกอริทึมแฮชที่มีการผสมและครอบคลุมสูงเช่น BuzHash (การใช้งานตัวอย่างที่http://www.java2s.com/Code/Java/Development-Class/AveryefficientjavahashalgorithmbasedontheBuzHashalgoritm.htm )

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.