Dize'deki Java hashCode () neden 31 çarpanı olarak kullanılır?


480

Java belgelerine göre, bir nesnenin karma koduString şu şekilde hesaplanır:

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

kullanarak intaritmetik, nerede s[i]olduğunu ben , dizenin inci karakter ndizesinin uzunluğu ve ^üs gösterir.

31 neden çarpan olarak kullanılıyor?

Çarpanın nispeten büyük bir asal sayı olması gerektiğini anlıyorum. Öyleyse neden 29, 37 veya 97 değil?


1
Ayrıca karşılaştırın stackoverflow.com/questions/1835976/… - Kendi hashCode işlevlerinizi yazarsanız 31'in kötü bir seçim olduğunu düşünüyorum.
Hans-Peter Störr

6
29, 37, hatta 97 olsaydı, 'neden 31 olmasın' diye soruyordun.
Lorne Marquis

2
@EJP hayır seçiminin arkasındaki nedeni bilmek önemlidir. sayı kara büyü hilesinin sonucu olmadığı sürece.
Dushyant Sabharwal

@ Peter-lawrey tarafından bu konuda bir blog yazısı var: vanilla-java.github.io/2018/08/12/… ve burada: vanilla-java.github.io/2018/08/15/…
Christophe Roussy

@DushyantSabharwal Benim nokta olabilirdi ki been çok pratik bir fark yapmadan, 29 veya 37 veya 97 veya 41 veya birçok başka değerler. 1976'da 37 kullanıyorduk.
Lorne Marquis

Yanıtlar:


405

Joshua Bloch'un Etkili Java'sına göre (yeterince önerilemeyen ve stackoverflow'daki sürekli sözler sayesinde satın aldığım bir kitap):

31 değeri seçildi çünkü garip bir asaltı. Eğer eşitse ve çarpma taşmış olsaydı, 2 ile çarpma kaymaya eşdeğer olduğu için bilgi kaybolurdu. Asal kullanmanın avantajı daha az açıktır, ancak gelenekseldir. 31 Güzel bir özellik çarpma bir değişim ve daha iyi performans için çıkarma ile değiştirilebilir olmasıdır: 31 * i == (i << 5) - i. Modern VM'ler bu tür optimizasyonu otomatik olarak yapar.

(Bölüm 3, Madde 9'dan: Eşit değerleri geçersiz kıldığınızda her zaman karma kodunu geçersiz kılın, sayfa 48)


346
Peki 2 hariç tüm asallar garip.
Kip

38
Bloch'un seçildiğini söylediğini sanmıyorum çünkü garip bir asaltı, ama garip VE çünkü asal olduğu için (VE kolayca bir vardiyaya / çıkarmaya optimize edilebildiğinden).
matt b

50
31 seçildi çünkü bu garip bir başbakan ??? Bu hiç mantıklı değil - 31'in en iyi dağıtımı sağladığı için seçildiğini söylüyorum - computinglife.wordpress.com/2008/11/20/…
computinglife

65
Bence 31 seçimi oldukça talihsiz. Elbette, eski makinelerde birkaç CPU çevrimi kaydedebilir, ancak zaten "@ ve #! Veya Ca ve DB gibi kısa ascii dizelerinde karma çarpışmalarınız var. Örneğin, 1327144003 veya en az 524287, aynı zamanda bit kaydırmaya da izin verir: 524287 * i == i << 19 - i.
Hans-Peter Störr

15
@Jason Cevabımı görün stackoverflow.com/questions/1835976/… . Demek istediğim: Eğer daha büyük bir başbakan kullanırsanız ve bu gün hiçbir şey kaybetmezseniz çok daha az çarpışma yaşarsınız. İngilizce olmayan dilleri ortak ascii olmayan karakterlerle kullanıyorsanız sorun daha da kötüleşir. Ve 31 kendi hashCode işlevlerini yazarken birçok programcı için kötü bir örnek oldu.
Hans-Peter Störr

80

As Goodrich ve Tamassia baştan 50.000 İngilizce kelimeleri alırsak, işaret sabitlerini 31, 33, 37, 39 kullanılarak, (Unix iki varyantı verilen kelime listelerinin birlik olarak oluşturulan) ve 41 den az 7 çarpışmalar üretecek herbir durumda. Bunu bilerek, birçok Java uygulamasının bu sabitlerden birini seçmesi şaşırtıcı değildir.

Tesadüfen, bu soruyu görünce "polinom karma kodları" bölümünü okudum.

EDIT: burada yukarıda bahsettiğim ~ 10mb PDF kitap bağlantısı. Bkz. Bölüm 10.2 Java'daki Veri Yapıları ve Algoritmaların Karma Tabloları (sayfa 413).


6
Ancak, ASCII aralığının dışında ortak karakterlere sahip herhangi bir uluslararası karakter kümesi kullanırsanız WAY daha fazla çarpışma yaşayabileceğinizi unutmayın. En azýndan bunu 31 ve Almanca için kontrol ettim. Bence 31 seçimi bozuldu.
Hans-Peter Störr

1
@jJack, Cevabınızda verilen bağlantı koptu.
SK Venkat

Bu yanıttaki her iki bağlantı da bozuk. Ayrıca, ilk paragraftaki tartışma biraz eksiktir; diğer tek sayılar bu karşılaştırmada listelediğiniz beşle nasıl karşılaştırılır?
Mark Amery

58

(Çoğunlukla) eski işlemcilerde, 31 ile çarpmak nispeten ucuz olabilir. Örneğin, bir ARM'de sadece bir talimattır:

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

Diğer işlemcilerin çoğu ayrı bir vardiya ve çıkarma talimatı gerektirir. Ancak çarpanınız yavaşsa bu hala bir kazançtır. Modern işlemciler hızlı çarpanlara sahip olma eğilimindedir, bu nedenle 32 doğru tarafa gittiği sürece fazla bir fark yaratmaz.

Harika bir karma algoritma değil, ama 1.0 kodundan daha iyi ve daha iyi (ve 1.0 özelliklerinden çok daha iyi!).


7
Oldukça komik, 31 ile çarpma masaüstü makinemde, örneğin 92821 ile çarpma işleminden biraz daha yavaş. Sanırım derleyici bunu vardiyaya ve optimize etmeye "optimize etmeye" çalışıyor. :-)
Hans-Peter Störr

1
+/- 255 aralığındaki tüm değerlerle aynı derecede hızlı olmayan bir ARM kullandığımı sanmıyorum. 2 eksi bir gücün kullanılması, iki değerle eşleşen bir değişikliğin karma kodunu iki güçle değiştirmesinin talihsiz bir etkisi vardır. -31 değeri daha iyi olurdu ve -83 (64 + 16 + 2 + 1) gibi bir şeyin daha iyi olabileceğini düşünürdüm (bitleri biraz daha iyi karıştırın).
supercat

@supercat Eksi ile ikna olmadım. Görünüşe göre sıfırlara geri döneceksin. / String.hashCodeIIRC, 8-bit çarpanı tanıtan ve muhtemelen kaydırma işlemleriyle birleştirilmiş aritmetik / mantık için iki döngüye çıkaran StrongARM'ı önceden kullanır.
Tom Hawtin - çakmak hattı

1
TomHawtin-tackline: 31 kullanıldığında, dört değerin karması 29791 * a + 961 * b + 31 * c + d olacaktır; -31 kullanılarak -29791 * a + 961 * b - 31 * c + d olur. Dört öğenin bağımsız olması durumunda farkın önemli olacağını düşünmüyorum, ancak bitişik öğelerin çiftleri eşleşirse, sonuçta ortaya çıkan karma kod, eşleştirilmemiş tüm öğelerin ve artı 32'nin (eşleştirilmiş olanlardan) bazı katları olacaktır. Dizeler için çok önemli olmayabilir, ancak birleştirme toplamaları için genel amaçlı bir yöntem yazıyorsa, bitişik öğelerin eşleştiği durum orantısız olarak yaygın olacaktır.
Supercat

3
@supercat eğlenceli gerçeği, hash kodu Map.Entryolmak için şartname ile düzeltildikey.hashCode() ^ value.hashCode() olarak buna rağmen bile bir sırasız çifti değildir keyve valuetamamen farklı bir anlama sahiptir. Evet, bu, Map.of(42, 42).hashCode()ya Map.of("foo", "foo", "bar", "bar").hashCode()da vs'nin tahmin edilebilir şekilde sıfır olduğu anlamına gelir . Bu yüzden haritaları diğer haritalar için anahtar olarak kullanmayın…
Holger

33

Çarparak bitler sola kaydırılır. Bu, mevcut karma kod alanından daha fazlasını kullanır ve çarpışmaları azaltır.

İki güç kullanılmadığında, alt sıradaki, en sağdaki bitler de, bir sonraki veri parçasının karmaya girmesi ile karıştırılmak üzere doldurulur.

İfade buna n * 31eşdeğerdir (n << 5) - n.


29

Bloch'un orijinal muhakemesini http://bugs.java.com/bugdatabase/view_bug.do?bug_id=4045622 adresindeki "Yorumlar" altında okuyabilirsiniz . Bir karma tablosunda ortaya çıkan "ortalama zincir boyutu" ile ilgili olarak farklı karma fonksiyonlarının performansını araştırdı. P(31)K & R'nin kitabında bulduğu ortak işlevlerden biriydi (ancak Kernighan ve Ritchie bile nereden geldiğini hatırlayamadı). Sonunda bir tane seçmek zorunda kaldı ve bu yüzden P(31)yeterince iyi performans gösterdiğinden beri aldı . P(33)Gerçekten daha kötü olmasa da ve 33 ile çarpmanın hesaplanması aynı derecede hızlıdır (sadece 5'lik bir değişim ve bir ekleme), 33'ü asal olmadığı için 31'i seçti:

Kalan dördünden, muhtemelen bir RISC makinesinde hesaplanması en ucuz olduğu için P (31) 'i seçerdim (çünkü 31 iki gücün iki gücünün farkıdır). P (33) hesaplamak benzer şekilde ucuzdur, ancak performansı marjinal olarak daha kötüdür ve 33 kompozittir, bu da beni biraz sinirlendirir.

Dolayısıyla, akıl yürütme, buradaki cevapların birçoğunun ima ettiği kadar rasyonel değildi. Ancak hepimiz bağırsak kararlarından sonra rasyonel nedenler bulmakta iyiyiz (ve Bloch bile buna eğilimli olabilir).


2
Kapsamlı bir araştırma ve tarafsız bir cevap!
Vishal K

22

Aslında, 37 oldukça iyi çalışır! z: = 37 * x şu şekilde hesaplanabilir:y := x + 8 * x; z := x + 4 * y . Her iki adım da bir LEA x86 yönergesine karşılık gelir, bu nedenle bu son derece hızlıdır.

Aslında, daha da büyük olan başbakan 73 ile çarpma ayarlanarak aynı hızda yapılabilir y := x + 8 * x; z := x + 8 * y.

73 veya 37 (31 yerine) kullanmak daha iyi olabilir, çünkü bu daha yoğun kodlara yol açar : İki LEA talimatı hareket için 6 bayt yerine 7 bayt alır, bu da 31 ile çarpma işlemi için shift + shift + çıkartmasıdır. Burada kullanılan 3 argümanlı LEA talimatları, Intel'in Sandy köprüsü mimarisinde yavaşladı ve 3 döngü gecikti.

Dahası, 73 Sheldon Cooper'ın favori numarası.


5
Bir pascal programcı mısınız? ne ile: = şeyler?
Mainguy

11
@Mainguy Aslında ALGOL sözdizimi ve sözde kodda oldukça sık kullanılıyor.
ApproachingDarknessFish

4
ancak ARM montajında ​​31 ile çarpma tek bir
komutta


In TPOP (1999) bir erken Java (s.57) okuyabilirsiniz: "... Sorun, (bir çarpanı ile göstermiştir bire bir eşdeğer karma değiştirerek çözüldü 37 ...)"
miku

19

Neil Coffey , 31'in neden önyargının ütülenmesi altında kullanıldığını açıklıyor .

Temel olarak 31 kullanmak karma fonksiyonu için daha eşit bir set-bit olasılık dağılımı sağlar.


12

Gönderen JDK-4045622 , Joshua Bloch o belirli (yeni) nedenlerini açıklar String.hashCode()uygulama seçildi

Aşağıdaki tablo, üç veri kümesi için yukarıda açıklanan çeşitli sağlama işlevlerinin performansını özetler:

1) Merriam-Webster'in 2. Uluslararası Kısaltılmamış Sözlüğünde (311.141 dizgi, ort. Uzunluk 10 karakter) girişleri olan tüm kelime ve deyimler.

2) / bin / , / usr / bin / , / usr / lib / , / usr / ucb / ve / usr / openwin / bin / * (66.304 karakter dizileri, ort. Uzunluk 21 karakter) içindeki tüm dizeler.

3) Dün gece saatlerce süren bir web tarayıcısı tarafından toplanan URL'lerin listesi (28.372 dize, ort. Uzunluk 49 karakter).

Tabloda gösterilen performans ölçüsü, karma tablodaki tüm öğelerin üzerindeki "ortalama zincir boyutu" dur (yani, bir öğeyi aramak için anahtar karşılaştırması sayısının beklenen değeri).

                          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

Bu tabloya bakıldığında, mevcut Java işlevi ve Weinberger'in işlevinin iki bozuk sürümü dışındaki tüm işlevlerin mükemmel, neredeyse ayırt edilemez performans sunduğu açıktır. Bu performansın esasen "teorik ideal" olduğuna inanıyorum, bir karma fonksiyonu yerine gerçek bir rasgele sayı üreteci kullanırsanız alacağınız şey budur.

WAIS işlevini, rasgele sayılar içeren sayfalar içerdiğinden ve performansı çok daha basit işlevlerden daha iyi olmadığından hariç tutacağım. Kalan altı işlevden herhangi biri mükemmel seçenekler gibi görünüyor, ancak bir tane seçmeliyiz. Sanırım Vo'nun varyantını ve Weinberger'in işlevini, küçük de olsa ek karmaşıklıkları nedeniyle dışlayacağım. Kalan dördünden, muhtemelen bir RISC makinesinde hesaplanması en ucuz olduğu için P (31) 'i seçerdim (çünkü 31 iki gücün iki gücünün farkıdır). P (33) hesaplamak benzer şekilde ucuzdur, ancak performansı marjinal olarak daha kötüdür ve 33 kompozittir, bu da beni biraz sinirlendirir.

alay etmek


5

Bloch tam olarak buna girmiyor, ama her zaman duyduğum / inandığım mantık bunun temel cebir olmasıdır. Hashler çarpma ve modül işlemlerine kaynar, bu da yardımcı olabilirseniz sayıları asla ortak faktörlerle kullanmak istemediğiniz anlamına gelir. Başka bir deyişle, göreceli asal sayılar cevapların eşit dağılımını sağlar.

Bir karma kullanarak oluşan sayılar genellikle:

  • içine yerleştirdiğiniz veri türünün modülü (2 ^ 32 veya 2 ^ 64)
  • hashtable'ınızdaki kova sayım modülü (değişkenlik gösterir. Java'da eskiden asaldı, şimdi 2 ^ n)
  • karıştırma işlevinizde sihirli bir sayı ile çarpma veya kaydırma
  • Giriş değeri

Gerçekten sadece bu değerlerin birkaçını kontrol edersiniz, bu yüzden biraz daha fazla dikkat gerektirir.


4

JDK'nın en son sürümünde, 31 hala kullanılmaktadır. https://docs.oracle.com/en/java/javase/12/docs/api/java.base/java/lang/String.html#hashCode ()

Hash dizesinin amacı

  • benzersiz ( ^hashcode hesaplama belgesinde operatöre bakalım , benzersiz yardımcı olur)
  • ucuz hesaplama maliyeti

31 maksimum değer 8 bit (= 1 byte) kayıtta koyabilirsiniz, en büyük asal sayı 1 bayt kayıtta koyabilir, tek sayıdır.

Çarpın 31 << 5 sonra kendini çıkarın, bu nedenle ucuz kaynaklara ihtiyaç duyar.


3

Emin değilim, ama onlar asal sayıların bazı örnek test ve 31 olası dizeleri bazı örnek üzerinde en iyi dağıtım verdiğini tahmin ediyorum.


1

Bunun nedeni, 31'in güzel bir özelliğe sahip olmasıdır - çarpımı, standart çarpmadan daha hızlı olan bir bitsel kaydırma ile değiştirilebilir:

31 * i == (i << 5) - i
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.