ทำไม hashCode ของ Java () ใน String ใช้ 31 เป็นตัวคูณ


480

สำหรับเอกสารคู่มือ Java รหัสแฮชสำหรับStringวัตถุนั้นคำนวณเป็น:

s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]

การใช้intเลขคณิตโดยที่s[i]เป็น ตัวอักษรที่iของสตริงnคือความยาวของสตริงและ^บ่งชี้การยกกำลัง

ทำไม 31 ถูกใช้เป็นตัวคูณ

ฉันเข้าใจว่าตัวคูณควรเป็นจำนวนเฉพาะที่ค่อนข้างใหญ่ ดังนั้นทำไมไม่ 29 หรือ 37 หรือ 97


1
เปรียบเทียบกับstackoverflow.com/questions/1835976/… - ฉันคิดว่า 31 เป็นตัวเลือกที่แย่ถ้าคุณเขียนฟังก์ชัน hashCode ของคุณเอง
Hans-Peter Störr

6
ถ้าเป็น 29 หรือ 37 หรือ 97 คุณจะถามว่า 'ทำไมไม่ 31'
มาร์ควิสแห่งลอร์น

2
@EJP เป็นสิ่งสำคัญที่ต้องทราบเหตุผลที่อยู่เบื้องหลังการเลือกไม่ นอกเสียจากว่าตัวเลขนั้นจะเป็นผลมาจากเวทมนต์ดำ
Dushyant Sabharwal

มีบล็อกโพสต์โดย @ peter-lawrey เกี่ยวกับที่นี่: vanilla-java.github.io/2018/08/12/…และที่นี่: vanilla-java.github.io/2018/08/15/…
Christophe Roussy

@ DushyantSabharwal ประเด็นของฉันคือว่ามันอาจเป็น 29 หรือ 37 หรือ 97 หรือ 41 หรือค่าอื่น ๆ อีกมากมายโดยไม่สร้างความแตกต่างในทางปฏิบัติ เราใช้งาน 37 ในปี 1976
มาร์ควิสแห่ง Lorne

คำตอบ:


405

อ้างอิงจากJava ที่มีประสิทธิภาพของ Joshua Bloch (หนังสือที่ไม่สามารถแนะนำได้มากพอและฉันซื้อมาขอบคุณที่กล่าวถึงอย่างต่อเนื่องใน stackoverflow):

ค่า 31 ถูกเลือกเนื่องจากเป็นค่าเฉพาะที่แปลก ถ้ามันเป็นเลขคู่และการคูณทวีคูณข้อมูลก็จะหายไปเนื่องจากการคูณด้วย 2 เท่ากับการเลื่อน ข้อดีของการใช้ไพรม์ไพร์สนั้นมีความชัดเจนน้อยกว่า แต่เป็นแบบดั้งเดิม คุณสมบัติที่ดีของวันที่ 31 31 * i == (i << 5) - iคือว่าคูณจะถูกแทนที่ด้วยการเปลี่ยนแปลงและการลบเพื่อให้ได้ประสิทธิภาพที่ดีกว่า: VM สมัยใหม่ทำการเพิ่มประสิทธิภาพประเภทนี้โดยอัตโนมัติ

(จากบทที่ 3 รายการ 9: แทนที่ hashcode ทุกครั้งเมื่อคุณแทนที่เท่ากับหน้า 48)


346
เวลาทั้งหมดนั้นแปลกมากยกเว้น 2 เพียงแค่บอกว่า
กี

38
ฉันไม่คิดว่าโบลชกำลังพูดว่าได้รับเลือกเพราะมันเป็นนายกที่แปลก แต่เพราะมันแปลกและเพราะมันเป็นนายก (และเพราะมันสามารถปรับให้เป็นกะ / ลบได้ง่าย)
matt b

50
วันที่ 31 ได้รับเลือกเพราะมันเป็นนายกที่แปลกประหลาด ??? ไม่สมเหตุสมผลเลย - ฉันบอกว่า 31 ถูกเลือกเพราะให้การกระจายที่ดีที่สุด - ตรวจสอบ computinglife.wordpress.com/2008/11/20/…
computinglife

65
ฉันคิดว่าตัวเลือกของ 31 ค่อนข้างโชคร้าย แน่นอนว่ามันอาจบันทึกซีพียูสักสองสามรอบบนเครื่องเก่า แต่คุณมีแฮชชนอยู่แล้วในสตริง ascii แบบสั้นเช่น "@ และ #! หรือ Ca และ DB สิ่งนี้จะไม่เกิดขึ้นหากคุณเลือกเช่น 1327144003 หรือที่ อย่างน้อย 524287 ซึ่งอนุญาตให้ bitshift: 524287 * i == i << 19 - i.
Hans-Peter Störr

15
@ สันดูคำตอบของฉันstackoverflow.com/questions/1835976/... ประเด็นของฉันคือคุณจะได้รับการชนน้อยลงถ้าคุณใช้นายกที่ใหญ่กว่าและไม่เสียอะไรเลยในวันนี้ ปัญหาจะเลวร้ายยิ่งขึ้นถ้าคุณใช้ภาษาที่ไม่ใช่ภาษาอังกฤษกับตัวอักษรที่ไม่ใช่ ASCII ทั่วไป และ 31 ทำหน้าที่เป็นตัวอย่างที่ไม่ดีสำหรับโปรแกรมเมอร์จำนวนมากเมื่อเขียนฟังก์ชัน hashCode ของตนเอง
Hans-Peter Störr

80

ดังที่Goodrich และ Tamassiaชี้ให้เห็นถ้าคุณใช้คำภาษาอังกฤษมากกว่า 50,000 คำ (เกิดขึ้นจากการรวมกันของรายการคำที่มีให้ใน Unix สองรูปแบบ) โดยใช้ค่าคงที่ 31, 33, 37, 39 และ 41 จะสร้างการชนกันน้อยกว่า 7 รายการ ในแต่ละกรณี. เมื่อรู้อย่างนี้แล้วก็ไม่น่าแปลกใจเลยที่การติดตั้ง Java จำนวนมากเลือกหนึ่งในค่าคงที่เหล่านี้

บังเอิญฉันอยู่กลางการอ่านหัวข้อ "รหัสแฮชโพลิโนเมียล" เมื่อฉันเห็นคำถามนี้

แก้ไข: นี่คือลิงก์ไปยังหนังสือ ~ 10mb PDF ที่ฉันอ้างถึงข้างต้น ดูส่วนที่ 10.2 ตารางแฮช (หน้า 413) ของโครงสร้างข้อมูลและอัลกอริทึมใน Java


6
อย่างไรก็ตามโปรดทราบว่าคุณอาจได้รับการชนกันมากขึ้น WAY หากคุณใช้ชุดอักขระสากลที่มีอักขระร่วมกันนอกช่วง ASCII อย่างน้อยฉันตรวจสอบสิ่งนี้เป็นวันที่ 31 และภาษาเยอรมัน ดังนั้นฉันคิดว่าการเลือก 31 ถูกทำลาย
Hans-Peter Störr

1
@jJack ลิงก์ที่ให้ไว้ในคำตอบของคุณเสีย
SK Venkat

ลิงก์ทั้งสองในคำตอบนี้ใช้งานไม่ได้ นอกจากนี้ข้อโต้แย้งในวรรคแรกยังไม่สมบูรณ์; ตัวเลขคี่อื่น ๆ เปรียบเทียบกับเลขห้าตัวที่คุณระบุไว้ในเกณฑ์มาตรฐานนี้ได้อย่างไร
Mark Amery

58

บนโปรเซสเซอร์เก่า (ส่วนใหญ่) การคูณด้วย 31 สามารถค่อนข้างถูก ตัวอย่างเช่นบน ARM มันเป็นเพียงคำสั่งเดียว:

RSB       r1, r0, r0, ASL #5    ; r1 := - r0 + (r0<<5)

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

มันไม่ใช่อัลกอริธึมแฮชที่ยอดเยี่ยม แต่ก็ดีพอและดีกว่าโค้ด 1.0 (และดีกว่าสเปค 1.0 มาก!)


7
ตลกพอการคูณด้วย 31 อยู่บนเครื่องเดสก์ท็อปของฉันจริง ๆ แล้วช้ากว่าการคูณด้วย 92821 ฉันเดาว่าคอมไพเลอร์พยายามที่จะ "เพิ่มประสิทธิภาพ" ให้เป็นกะและเพิ่มเช่นกัน :-)
Hans-Peter Störr

1
ฉันไม่คิดว่าฉันเคยใช้ ARM ซึ่งไม่เร็วเท่ากันกับค่าทั้งหมดในช่วง +/- 255 การใช้กำลัง 2 ลบหนึ่งมีผลที่น่าเสียดายที่การเปลี่ยนแปลงการจับคู่กับสองค่าเปลี่ยนรหัสแฮชด้วยกำลังสอง ค่าของ -31 จะดีขึ้นและฉันคิดว่าบางอย่างเช่น -83 (64 + 16 + 2 + 1) อาจจะดีกว่านี้ (บิตการปั่นค่อนข้างดีขึ้น)
supercat

@supercat ไม่เชื่อมั่นในเครื่องหมายลบ ดูเหมือนว่าคุณจะมุ่งหน้ากลับไปที่ศูนย์ / String.hashCodeถือกำเนิด StrongARM ซึ่ง IIRC แนะนำตัวคูณ 8 บิตและอาจเพิ่มเป็นสองรอบสำหรับการคำนวณทางคณิตศาสตร์ / ตรรกะพร้อมการดำเนินการ shift
Tom Hawtin - tackline

1
@ TomHawtin-tackline: การใช้ 31 แฮชของสี่ค่าจะเป็น 29791 * a + 961 * b + 31 * c + d; โดยใช้ -31 มันจะเป็น -29791 * a + 961 * b - 31 * c + d ฉันไม่คิดว่าความแตกต่างจะมีนัยสำคัญหากสี่รายการนั้นเป็นอิสระ แต่หากคู่ของรายการที่อยู่ติดกันตรงกันรหัสแฮชที่ได้จะเป็นผลงานของรายการที่ไม่ได้รับการจับคู่ทั้งหมดบวก 32 หลายตัว สำหรับสตริงมันอาจไม่สำคัญมากนัก แต่หากมีวิธีการเขียนสำหรับวัตถุประสงค์ทั่วไปสำหรับการรวมการแปลงแป้นพิมพ์สถานการณ์ที่รายการที่อยู่ติดกันจะตรงกันกันอย่างไม่เป็นสัดส่วน
supercat

3
@supercat ความจริงสนุกรหัส hash ของMap.Entryได้รับการแก้ไขตามข้อกำหนดkey.hashCode() ^ value.hashCode()แม้จะไม่ได้เป็นคู่ที่ไม่มีการเรียงลำดับตามkeyและvalueมีความหมายที่แตกต่างกันโดยสิ้นเชิง ใช่นั่นหมายถึงว่าMap.of(42, 42).hashCode()หรือMap.of("foo", "foo", "bar", "bar").hashCode()ฯลฯ เป็นศูนย์ที่คาดการณ์ได้ ดังนั้นอย่าใช้แผนที่เป็นกุญแจสำหรับแผนที่อื่น ๆ ...
Holger

33

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

หากไม่ใช้กำลังสองบิตบิตล่างขวาสุดจะถูกเติมด้วยเช่นกันเพื่อผสมกับข้อมูลชิ้นถัดไปที่จะเข้าสู่แฮช

การแสดงออกเทียบเท่ากับn * 31(n << 5) - n


29

คุณสามารถอ่านเหตุผลเดิม Bloch ภายใต้ "ความคิดเห็น" ในhttp://bugs.java.com/bugdatabase/view_bug.do?bug_id=4045622 เขาตรวจสอบประสิทธิภาพการทำงานของฟังก์ชันแฮชที่แตกต่างกันโดยคำนึงถึง "ขนาดโซ่เฉลี่ย" ที่เกิดขึ้นในตารางแฮช P(31)เป็นหนึ่งในหน้าที่ทั่วไปในช่วงเวลานั้นซึ่งเขาพบในหนังสือของ K&R (แต่ Kernighan และ Ritchie จำไม่ได้ว่ามันมาจากไหน) ในที่สุดเขาก็ต้องเลือกอย่างใดอย่างหนึ่งดังนั้นเขาจึงใช้P(31)เพราะมันดูเหมือนจะทำงานได้ดีพอ แม้ว่าจะP(33)ไม่ได้เลวร้ายลงและการคูณด้วย 33 ก็เร็วพอ ๆ กันในการคำนวณ (เพียงแค่เปลี่ยนจาก 5 และเพิ่ม) เขาเลือกที่ 31 เพราะ 33 ไม่ได้เป็นนายก:

จากสี่ส่วนที่เหลือฉันอาจเลือก P (31) เนื่องจากมันถูกที่สุดในการคำนวณบนเครื่อง RISC (เพราะ 31 คือความแตกต่างของสองพลังของสอง) P (33) มีราคาถูกในทำนองเดียวกันในการคำนวณ แต่ประสิทธิภาพการทำงานนั้นแย่ลงเล็กน้อยและ 33 เป็นคอมโพสิตซึ่งทำให้ฉันกังวลเล็กน้อย

ดังนั้นการให้เหตุผลก็ไม่สมเหตุสมผลเหมือนคำตอบมากมายที่นี่ดูเหมือนจะบอกเป็นนัย แต่เราทุกคนเก่งในการหาเหตุผลที่มีเหตุผลหลังจากการตัดสินใจของลำไส้ (และแม้แต่ Bloch ก็มีแนวโน้มที่จะเป็นเช่นนั้น)


2
การวิจัยอย่างละเอียดและคำตอบที่เป็นกลาง!
Vishal K

22

ที่จริงแล้ว 37 ก็ใช้ได้ดีทีเดียว! z: = 37 * x สามารถคำนวณได้y := x + 8 * x; z := x + 4 * yดังนี้ ทั้งสองขั้นตอนนั้นสอดคล้องกับคำแนะนำ LEA x86 หนึ่งคำสั่งดังนั้นจึงรวดเร็วมาก

อันที่จริงแล้วการคูณด้วยนายกยิ่งใหญ่ยิ่งกว่า73y := x + 8 * x; z := x + 8 * yสามารถทำได้ที่ความเร็วเท่ากันโดยการตั้งค่า

การใช้ 73 หรือ 37 (แทน 31) อาจจะดีกว่าเพราะมันจะนำไปสู่ รหัสที่มีความหนาแน่นมากขึ้น : คำสั่ง LEA สองคำสั่งใช้เวลาเพียง 6 ไบต์เทียบกับ 7 ไบต์สำหรับการย้าย + shift + ลบสำหรับการคูณ 31 ข้อสังเกตที่เป็นไปได้คือ คำแนะนำ 3 ข้อโต้แย้งของ LEA ที่ใช้ที่นี่กลายเป็นช้าลงในสถาปัตยกรรม Sandy Bridge ของ Intel ด้วยเวลาแฝงที่เพิ่มขึ้น 3 รอบ

ยิ่งไปกว่านั้น73คือหมายเลขที่ชื่นชอบของเชลดอนคูเปอร์


5
คุณเป็นโปรแกรมเมอร์ปาสคาลหรือเปล่า? มีอะไร: = stuff?
Mainguy

11
@ Mainguy จริงๆแล้วมันเป็นไวยากรณ์ ALGOL และมีการใช้งานค่อนข้างบ่อยในรหัสหลอก
ApproachingDarknessFish

4
แต่ในการคูณแอสเซมบลี ARM ด้วย 31 สามารถทำได้ในคำสั่งเดียว
phuclv


ในTPOP (1999) มีใครสามารถอ่านเกี่ยวกับ Java ช่วงต้น (หน้า 57): "... ปัญหาได้รับการแก้ไขโดยการแทนที่แฮชเป็นหนึ่งเท่ากับที่เราได้แสดง (ด้วยตัวคูณ37 ) ... "
Miku

19

นีลเบนจามินอธิบายว่าทำไม 31 ถูกใช้ภายใต้รองจากอคติ

โดยทั่วไปการใช้ 31 จะให้การกระจายความน่าจะเป็นแบบ set-bit ที่มากกว่าสำหรับฟังก์ชันแฮช


12

จากJDK-4045622โดยที่ Joshua Bloch อธิบายถึงสาเหตุที่String.hashCode()การเลือกใช้งาน(ใหม่) นั้น

ตารางด้านล่างสรุปประสิทธิภาพของฟังก์ชันแฮชต่างๆที่อธิบายไว้ข้างต้นสำหรับชุดข้อมูลสามชุด:

1) คำและวลีทั้งหมดที่มีรายการในพจนานุกรม Int'l Unabridged Dictionary ที่สองของ Merriam-Webster (311,141 สตริง, ความยาวเฉลี่ย 10 ตัวอักษร)

2) สตริงทั้งหมดใน / bin / , / usr / bin / , / usr / lib / , / usr / ucb / และ / usr / openwin / bin / * (66,304 สตริง, ความยาวเฉลี่ย 21 ตัวอักษร)

3) รายการ URL ที่รวบรวมโดยโปรแกรมรวบรวมข้อมูลเว็บซึ่งใช้เวลาหลายชั่วโมงในคืนที่ผ่านมา (28,372 สตริงความยาวเฉลี่ย 49 อักขระ)

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

                          Webster's   Code Strings    URLs
                          ---------   ------------    ----
Current Java Fn.          1.2509      1.2738          13.2560
P(37)    [Java]           1.2508      1.2481          1.2454
P(65599) [Aho et al]      1.2490      1.2510          1.2450
P(31)    [K+R]            1.2500      1.2488          1.2425
P(33)    [Torek]          1.2500      1.2500          1.2453
Vo's Fn                   1.2487      1.2471          1.2462
WAIS Fn                   1.2497      1.2519          1.2452
Weinberger's Fn(MatPak)   6.5169      7.2142          30.6864
Weinberger's Fn(24)       1.3222      1.2791          1.9732
Weinberger's Fn(28)       1.2530      1.2506          1.2439

ดูที่ตารางนี้เห็นได้ชัดว่าฟังก์ชั่นทั้งหมดยกเว้นฟังก์ชั่น Java ปัจจุบันและฟังก์ชั่น Weinberger ทั้งสองรุ่นที่หักนั้นให้ประสิทธิภาพที่ยอดเยี่ยมและแทบแยกไม่ออก ฉันคาดเดาอย่างมากว่าการแสดงนี้เป็น "อุดมคติเชิงทฤษฎี" ซึ่งเป็นสิ่งที่คุณจะได้รับหากคุณใช้ตัวสร้างตัวเลขสุ่มจริงแทนฟังก์ชันแฮช

ฉันจะแยกแยะฟังก์ชั่น WAIS เนื่องจากสเปคของมันมีหน้าของตัวเลขสุ่มและประสิทธิภาพของมันไม่ได้ดีไปกว่าฟังก์ชั่นที่ง่ายกว่าใด ๆ ฟังก์ชั่นทั้งหกที่เหลืออยู่ดูเหมือนจะเป็นตัวเลือกที่ยอดเยี่ยม แต่เราต้องเลือกอย่างใดอย่างหนึ่ง ฉันคิดว่าฉันตัดทอนตัวแปรของ Vo และฟังก์ชั่นของ Weinberger เนื่องจากความซับซ้อนที่เพิ่มเข้ามาของพวกเขา จากสี่ส่วนที่เหลือฉันอาจเลือก P (31) เนื่องจากมันถูกที่สุดในการคำนวณบนเครื่อง RISC (เพราะ 31 คือความแตกต่างของสองพลังของสอง) P (33) มีราคาถูกในทำนองเดียวกันในการคำนวณ แต่ประสิทธิภาพการทำงานนั้นแย่ลงเล็กน้อยและ 33 เป็นคอมโพสิตซึ่งทำให้ฉันกังวลเล็กน้อย

หยอกเย้า


5

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

หมายเลขที่ใช้ในการแฮชคือ:

  • โมดูลัสของชนิดข้อมูลที่คุณใส่เข้าไป (2 ^ 32 หรือ 2 ^ 64)
  • โมดูลัสของถังนับใน hashtable ของคุณ (แตกต่างกันไปในจาวาเคยเป็นนายกตอนนี้ 2 ^ n)
  • คูณหรือเลื่อนด้วยหมายเลขเวทย์มนตร์ในฟังก์ชั่นการผสมของคุณ
  • ค่าอินพุต

คุณสามารถควบคุมค่าเหล่านี้ได้สองสามค่าเท่านั้นดังนั้นการดูแลเป็นพิเศษจะเกิดขึ้น


4

ใน JDK รุ่นล่าสุดยังคงใช้ 31 https://docs.oracle.com/en/java/javase/12/docs/api/java.base/java/lang/String.html#hashCode ()

วัตถุประสงค์ของสตริงแฮชคือ

  • ที่ไม่ซ้ำกัน (ให้ดูผู้ประกอบการ^ในเอกสารการคำนวณ hashcode ซึ่งจะช่วยให้ไม่ซ้ำกัน)
  • ราคาถูกสำหรับการคำนวณ

31 คือค่าสูงสุดสามารถใส่ลงทะเบียน 8 บิต (= 1 ไบต์) เป็นหมายเลขเฉพาะที่ใหญ่ที่สุดสามารถใส่ในการลงทะเบียน 1 ไบต์เป็นเลขคี่

ทวีคูณ 31 คือ << 5 จากนั้นก็ลบมันออกไปดังนั้นต้องใช้ทรัพยากรราคาถูก


3

ฉันไม่แน่ใจ แต่ฉันเดาว่าพวกเขาทดสอบตัวอย่างของจำนวนเฉพาะและพบว่า 31 ให้การแจกแจงที่ดีที่สุดกับตัวอย่างของสตริงที่เป็นไปได้


1

นี่เป็นเพราะ 31 มีคุณสมบัติที่ดี - การคูณสามารถถูกแทนที่ด้วยการเลื่อนบิตที่เร็วกว่าการคูณมาตรฐาน:

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