Bir tamsayı karma anahtarını kabul eden hangi tamsayı hash işlevi iyidir?


Yanıtlar:


47

Knuth'un çarpımsal yöntemi:

hash(i)=i*2654435761 mod 2^32

Genel olarak, karma boyutunuza göre ( 2^32örnekte) ve onunla ortak faktörleri olmayan bir çarpan seçmelisiniz . Bu şekilde, karma işlevi tüm karma alanınızı eşit şekilde kaplar.

Düzenleme: Bu hash fonksiyonunun en büyük dezavantajı, bölünebilirliği korumasıdır, bu nedenle tam sayılarınızın tümü 2'ye veya 4'e bölünebiliyorsa (bu nadir değildir), hash'leri de olacaktır. Bu, hash tablolarında bir sorundur - kullanılan kovaların yalnızca 1 / 4'ünü elde edebilirsiniz.


36
Ünlü bir isme bağlı olsa da, gerçekten kötü bir hash işlevi.
Seun Osewa

5
Asal tablo boyutlarıyla kullanıldığında hiç de kötü bir hash işlevi değildir. Ayrıca, kapalı hashing içindir . Karma değerleri tek tip olarak dağıtılmazsa, çarpımsal karma oluşturma, bir değerden gelen çarpışmaların diğer karma değerlerle öğeleri "rahatsız etme" olasılığının düşük olmasını sağlar.
Paolo Bonzini


7
Paolo: Knuth'un yöntemi, üst bitlerde çığ
düşmemesi

9
Daha yakından incelendiğinde, 2654435761'in aslında bir asal sayı olduğu ortaya çıkıyor. O 2654435769. yerine seçildi muhtemelen neden Yani
karadoc

149

Aşağıdaki algoritmanın çok iyi bir istatistiksel dağılım sağladığını buldum. Her giriş biti, her çıkış bitini yaklaşık% 50 olasılıkla etkiler. Çarpışma yoktur (her girdi farklı bir çıktıyla sonuçlanır). Algoritma, CPU'nun yerleşik bir tam sayı çarpma birimine sahip olmaması dışında hızlıdır. C kodu, varsayılarak int32 bit (Java, değiştirin >>ile >>>ve kaldırma unsigned):

unsigned int hash(unsigned int x) {
    x = ((x >> 16) ^ x) * 0x45d9f3b;
    x = ((x >> 16) ^ x) * 0x45d9f3b;
    x = (x >> 16) ^ x;
    return x;
}

Sihirli sayı, saatlerce çalışan ve çığ etkisini (tek bir giriş biti değiştirildiğinde değişen çıktı bitlerinin sayısı; ortalama olarak yaklaşık 16 olmalıdır) hesaplayan özel bir çok iş parçacıklı test programı kullanılarak hesaplandı. çıkış biti değişiklikleri (çıkış bitleri birbirine bağlı olmamalıdır) ve herhangi bir giriş biti değiştirilirse her çıkış bitindeki bir değişiklik olasılığı. Hesaplanan değerler, MurmurHash tarafından kullanılan 32-bit sonlandırıcıdan daha iyidir ve neredeyse AES kullanılırkenki kadar iyidir (tam olarak değil) . Küçük bir avantaj, aynı sabitin iki kez kullanılmasıdır (son test ettiğimde biraz daha hızlı hale getirdi, hala böyle olup olmadığından emin değilim).

Şunu ( çarpımsal tersi ) 0x45d9f3bile değiştirirseniz, işlemi tersine çevirebilirsiniz (hash'den girdi değerini elde edebilirsiniz ):0x119de1f3

unsigned int unhash(unsigned int x) {
    x = ((x >> 16) ^ x) * 0x119de1f3;
    x = ((x >> 16) ^ x) * 0x119de1f3;
    x = (x >> 16) ^ x;
    return x;
}

64 bitlik sayılar için, en hızlı olmayabileceğini düşünseniz bile aşağıdakileri kullanmanızı öneririm. Bu , Better Bit Mixing (mix 13) adlı blog makalesine dayanıyor gibi görünen splitmix64'e dayanıyor .

uint64_t hash(uint64_t x) {
    x = (x ^ (x >> 30)) * UINT64_C(0xbf58476d1ce4e5b9);
    x = (x ^ (x >> 27)) * UINT64_C(0x94d049bb133111eb);
    x = x ^ (x >> 31);
    return x;
}

Java, kullanım için long, eklemek Lyerine, sabite >>ile >>>ve çıkarın unsigned. Bu durumda, ters çevirmek daha karmaşıktır:

uint64_t unhash(uint64_t x) {
    x = (x ^ (x >> 31) ^ (x >> 62)) * UINT64_C(0x319642b2d24d8ec3);
    x = (x ^ (x >> 27) ^ (x >> 54)) * UINT64_C(0x96de1b173f119089);
    x = x ^ (x >> 30) ^ (x >> 60);
    return x;
}

Güncelleme: Diğer (muhtemelen daha iyi) sabitlerin listelendiği Hash Function Prospector projesine de bakmak isteyebilirsiniz .


2
ilk iki satır tamamen aynı! burada bir yazım hatası var mı?
Kshitij Banerjee

3
Hayır, bu bir yazım hatası değil, ikinci satır daha fazla bitleri karıştırıyor. Tek bir çarpma işlemi kullanmak o kadar iyi değildir.
Thomas Mueller

3
Sihirli sayıyı değiştirdim çünkü bir test senaryosuna göre 0x45d9f3b değerini yazdığım daha iyi bir kafa karışıklığı ve difüzyon sağlar , özellikle bir çıkış biti değişirse, her bir diğer çıkış biti yaklaşık aynı olasılıkla değişir (tüm çıkış bitlerine ek olarak, bir giriş biti değişirse aynı olasılık). 0x3335b369'un sizin için daha iyi çalıştığını nasıl ölçtünüz? Bir int 32 bit sizin için mi?
Thomas Mueller

3
64 bit unsigned int - 32 bit unsigned int için güzel bir hash fonksiyonu arıyorum. Bu durumda yukarıdaki sihirli sayı aynı mı olacak? 16 bit yerine 32 bit kaydırdım.
alessandro

3
Bu durumda daha büyük bir faktörün daha iyi olacağına inanıyorum, ancak bazı testler yapmanız gerekecek. Veya (benim yaptığım budur) önce kullanın x = ((x >> 32) ^ x)ve ardından yukarıdaki 32 bit çarpımlarını kullanın. Hangisinin daha iyi olduğundan emin değilim. Ayrıca Murmur3
Thomas Mueller

29

Verilerinizin nasıl dağıtıldığına bağlıdır. Basit bir sayaç için en basit işlev

f(i) = i

iyi olacak (optimal olduğundan şüpheleniyorum ama bunu kanıtlayamıyorum).


3
Bununla ilgili sorun, ortak bir faktörle (kelime hizalı bellek adresleri vb.) Bölünebilen büyük tam sayı kümelerine sahip olmanın yaygın olmasıdır. Şimdi, hash tablonuz aynı faktörle bölünebiliyorsa, yalnızca yarım (veya 1/4, 1/8, vb.) Kova kullanılır.
Rafał Dowgird

8
@Rafal: Bu yüzden yanıt "basit bir sayaç için" ve "Verilerinizin nasıl dağıtıldığına bağlı" diyor
erikkallen


5
@JuandeCarrion Bu yanıltıcı çünkü kullanılan karma o değil. İki tablo boyutunun gücünü kullanmaya geçtikten sonra, Java geri dönen her hashı yeniden oluşturur .hashCode(), buraya bakın .
Esailija 01

8
Özdeşlik işlevi, dağıtım özellikleri (veya eksikliği) nedeniyle birçok pratik uygulamada bir karma olarak oldukça kullanışsızdır, tabii ki yerellik istenen bir özellik
değilse

12

Hızlı ve iyi hash fonksiyonları, daha az kaliteye sahip hızlı permütasyonlardan oluşturulabilir.

  • düzensiz tamsayı ile çarpma
  • ikili dönüşler
  • xorshift

Şununla gösterildiği gibi, üstün niteliklere sahip bir hash işlevi sağlamak için Rastgele sayı üretimi için PCG .

Aslında bu aynı zamanda rrxmrrxmsx_0 ve üfürüm hash'in bilerek veya bilmeyerek kullandığı tariftir.

Şahsen buldum

uint64_t xorshift(const uint64_t& n,int i){
  return n^(n>>i);
}
uint64_t hash(const uint64_t& n){
  uint64_t p = 0x5555555555555555ull; // pattern of alternating 0 and 1
  uint64_t c = 17316035218449499591ull;// random uneven integer constant; 
  return c*xorshift(p*xorshift(n,32),32);
}

yeterince iyi olmak için.

İyi bir hash işlevi,

  1. Mümkünse bilgi kaybetmemek için önyargılı olun ve en az çarpışmaya sahip olun
  2. mümkün olduğu kadar çok ve eşit şekilde kademelendirin, yani her giriş biti her çıkış bitini 0.5 olasılıkla çevirmelidir.

Önce kimlik işlevine bakalım. 1. tatmin eder ama 2. değildir:

kimlik işlevi

Giriş biti n,% 100 (kırmızı) bir korelasyon ile çıkış biti n'yi belirler ve diğerleri yoktur, bu nedenle bunlar mavidir ve boyunca mükemmel bir kırmızı çizgi verir.

Bir xorshift (n, 32) çok daha iyi değildir, bir buçuk çizgi verir. Yine de tatmin edici 1., çünkü ikinci bir uygulamayla ters çevrilebilir.

xorshift

İşaretsiz bir tamsayı ile çarpma çok daha iyidir, daha güçlü bir şekilde basamaklanır ve 0,5 olasılıkla daha fazla çıktı bitini çevirir, yeşil renkte istediğiniz şey budur. Her bir eşit olmayan tam sayı için çarpımsal bir tersi olduğu için 1'i karşılar.

Knuth

İkisini birleştirmek, aşağıdaki çıktıyı verir, ancak iki önyargılı işlevin bileşimi başka bir önyargılı işlev verir.

knuth • xorshift

İkinci bir çarpma ve xorshift uygulaması aşağıdakileri verecektir:

önerilen karma

Veya GHash gibi Galois alan çarpımlarını kullanabilirsiniz , bunlar modern CPU'larda oldukça hızlı hale gelirler ve tek adımda üstün niteliklere sahiptirler.

   uint64_t const inline gfmul(const uint64_t& i,const uint64_t& j){           
     __m128i I{};I[0]^=i;                                                          
     __m128i J{};J[0]^=j;                                                          
     __m128i M{};M[0]^=0xb000000000000000ull;                                      
     __m128i X = _mm_clmulepi64_si128(I,J,0);                                      
     __m128i A = _mm_clmulepi64_si128(X,M,0);                                      
     __m128i B = _mm_clmulepi64_si128(A,M,0);                                      
     return A[0]^A[1]^B[1]^X[0]^X[1];                                              
   }

gfmul: Kod sahte kod gibi görünüyor, çünkü afaik __m128i ile parantez kullanamazsınız. Hala çok ilginç. İlk satır, "birimselleştirilmiş bir __m128i (I) al ve onu (parametre) i ile xor al" diyor. Bunu I ile başlat ve i ile xor i ile başlatmalı mıyım? Öyleyse, i ile yük I ile aynı mı olur? ve ben bir değil (işlem)?
Jan

@Jan yapmak istediğim şey __m128i I = i; //set the lower 64 bits, ama yapamıyorum, bu yüzden kullanıyorum ^=. 0^1 = 1bu nedenle dahil değildir. {}Derleyicimle başlatmaya gelince asla şikayet etmedi, bu en iyi çözüm olmayabilir, ancak bununla istediğim şey hepsini 0'a başlatmak, böylece yapabilirim ^=veya |=. Sanırım bu kodu, aynı zamanda ters çevirme sağlayan bu blog gönderisine dayandırdım , çok faydalı: D
Wolfgang Brehm

6

Bu sayfa , genel olarak düzgün bir şekilde olma eğiliminde olan bazı basit hash fonksiyonlarını listeler, ancak herhangi bir basit hash'in iyi çalışmadığı patolojik durumlar vardır.


6
  • 32 bit çarpma yöntemi (çok hızlı) bkz. @Rafal

    #define hash32(x) ((x)*2654435761)
    #define H_BITS 24 // Hashtable size
    #define H_SHIFT (32-H_BITS)
    unsigned hashtab[1<<H_BITS]  
    .... 
    unsigned slot = hash32(x) >> H_SHIFT
  • 32 bit ve 64 bit (iyi dağıtım) şurada : MurmurHash

  • Tamsayı Hash Fonksiyonu

3

Eternally Confuzzled'da bazı karma algoritmalar hakkında güzel bir genel bakış var . Bob Jenkins'in hızla çığa ulaşan ve bu nedenle verimli hash tablosu araması için kullanılabilen bir seferde tek seferde hashini öneririm.


4
Bu iyi bir makale, ancak tamsayılara değil, dize anahtarlarının hashingine odaklanıyor.
Adrian Mouat

Daha açık olmak gerekirse, makaledeki yöntemler tamsayılar için işe yarasa (veya uyarlanabilse de), tamsayılar için daha verimli algoritmalar olduğunu varsayıyorum.
Adrian Mouat

2

Cevap, aşağıdaki gibi birçok şeye bağlıdır:

  • Nerede kullanmayı düşünüyorsunuz?
  • Hash ile ne yapmaya çalışıyorsun?
  • Kriptografik olarak güvenli bir hash fonksiyonuna ihtiyacınız var mı?

SHA-1 gibi Merkle-Damgard hash fonksiyon ailesine bir göz atmanızı öneririm .


1

Verilerinizi önceden bilmeden bir hash fonksiyonunun "iyi" olduğunu söyleyebileceğimizi sanmıyorum! ve onunla ne yapacağını bilmeden.

Bilinmeyen veri boyutları için karma tablolardan daha iyi veri yapıları vardır (burada bir karma tablo için karma işlemi yaptığınızı varsayıyorum). Sınırlı miktarda bellekte depolanması gereken "sonlu" sayıda öğeye sahip olduğumu bildiğimde kişisel olarak bir karma tablo kullanırdım. Karma fonksiyonum hakkında düşünmeye başlamadan önce verilerim üzerinde hızlı bir istatistiksel analiz yapmayı dener ve yapar, nasıl dağıtıldığını görmek vb.


1

Rastgele hash değerleri için, bazı mühendisler altın oran asal sayısının (2654435761) kötü bir seçim olduğunu söyledi, test sonuçlarımla bunun doğru olmadığını anladım; bunun yerine, 2654435761 hash değerlerini oldukça iyi dağıtır.

#define MCR_HashTableSize 2^10

unsigned int
Hash_UInt_GRPrimeNumber(unsigned int key)
{
  key = key*2654435761 & (MCR_HashTableSize - 1)
  return key;
}

Karma tablo boyutu ikinin üssü olmalıdır.

Tamsayılar için birçok hash fonksiyonunu değerlendirmek için bir test programı yazdım, sonuçlar GRPrimeNumber'ın oldukça iyi bir seçim olduğunu gösteriyor.

Denedim:

  1. total_data_entry_number / total_bucket_number = 2, 3, 4; total_bucket_number = karma tablo boyutu;
  2. karma değer alanını kova dizin alanına eşleyin; yani, Hash_UInt_GRPrimeNumber () 'da gösterildiği gibi, hash değerini Logical And Operation ile (hash_table_size - 1) ile paket dizinine dönüştürün;
  3. her bir kepçenin çarpışma sayısını hesaplayın;
  4. eşlenmemiş paketi, yani boş bir paketi kaydedin;
  5. tüm kovaların maksimum çarpışma sayısını öğrenin; yani en uzun zincir uzunluğu;

Test sonuçlarımla, Altın Oran Asal Sayısının her zaman daha az boş kova veya sıfır boş kova ve en kısa çarpışma zinciri uzunluğuna sahip olduğunu buldum.

Tamsayılar için bazı hash işlevlerinin iyi olduğu iddia edilmektedir, ancak test sonuçları, total_data_entry / total_bucket_number = 3 olduğunda, en uzun zincir uzunluğunun 10'dan büyük olduğunu (maks. Çarpışma sayısı> 10) ve birçok kepçenin eşlenmediğini (boş kovalar ), sıfır boş kova ve Altın Oran Asal Sayı Karma ile en uzun zincir uzunluğu 3 sonucuyla karşılaştırıldığında çok kötü.

BTW, test sonuçlarımla, xor hash işlevlerini kaydırmanın bir versiyonunun oldukça iyi olduğunu buldum (mikera tarafından paylaşılıyor).

unsigned int Hash_UInt_M3(unsigned int key)
{
  key ^= (key << 13);
  key ^= (key >> 17);    
  key ^= (key << 5); 
  return key;
}

2
Ama o zaman neden ürünü doğru kaydırmıyorsunuz ki en çok karışık olan bitleri koruyun? Çalışması gereken yol buydu
Harold

1
@harold, altın oran asal sayısı dikkatlice seçildi, ancak bunun bir fark yaratmayacağını düşünüyorum, ancak "en karışık bitler" ile çok daha iyi olup olmadığını görmek için test edeceğim. Demek istediğim, "Bu iyi bir seçim değil." bu doğru değil, test sonuçlarının gösterdiği gibi, sadece bitlerin alt kısmını kapmak yeterince iyidir ve hatta birçok karma fonksiyondan daha iyidir.
Chen-ChungChia

(2654435761, 4295203489) altın asal oranıdır.
Chen-ChungChia

(1640565991, 2654435761) aynı zamanda altın asal oranıdır.
Chen-ChungChia

@harold, Ürünü doğru kaydırmak daha da kötüleşir, sadece 1 pozisyon sağa kaydırılsa bile (2'ye bölünür), yine de kötüleşir (yine de sıfır boş kova, ancak en uzun zincir uzunluğu daha büyüktür); daha fazla pozisyon sağa kayınca sonuç daha da kötüleşir. Neden? Sanırım sebebi şudur: Ürünü doğru kaydırmak daha fazla hash değerinin coprime olmamasını sağlar, sadece benim tahminim, gerçek sebep sayı teorisini içerir.
Chen-ChungChia

1

Bu konuyu bulduğumdan beri kullanıyorum splitmix64(Thomas Mueller'in cevabına işaret ediyor ). Bununla birlikte, yakın zamanda Pelle Evensen'in orijinal MurmurHash3 sonlandırıcısından ve onun haleflerinden ( splitmix64ve diğer karışımlardan) çok daha iyi istatistiksel dağılım sağlayan rrxmrrxmsx_0'e rastladım . İşte C'deki kod parçacığı:

#include <stdint.h>

static inline uint64_t ror64(uint64_t v, int r) {
    return (v >> r) | (v << (64 - r));
}

uint64_t rrxmrrxmsx_0(uint64_t v) {
    v ^= ror64(v, 25) ^ ror64(v, 50);
    v *= 0xA24BAED4963EE407UL;
    v ^= ror64(v, 24) ^ ror64(v, 49);
    v *= 0x9FB21C651E98DF25UL;
    return v ^ v >> 28;
}

Pelle ayrıca son adımda kullanılan 64 bitlik karıştırıcının ve daha yeni varyantların derinlemesine bir analizini sağlarMurmurHash3 .


2
Bu işlev önyargılı değildir. V = ror (v, 25) olan tüm v için, yani tüm 0 ve tümü 1, iki yerde aynı çıktıyı üretecektir. En az iki tane daha olan ve v = ror (v, 28) ile aynı olan tüm v = ror64 (v, 24) ^ ror64 (v, 49) değerleri için, 2 ^ 4 daha verir, yaklaşık 22 gereksiz çarpışma . Splitmix'in iki uygulaması muhtemelen aynı derecede iyi ve aynı derecede hızlıdır, ancak yine de tersine çevrilebilir ve çarpışmasızdır.
Wolfgang Brehm
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.