Yakın zamana kadar cevabım Jon Skeet'e çok yakın olacaktı. Ancak, son zamanlarda iki güç tablosunu kullanan, yani iç tablonun büyüklüğünün 8, 16, 32, vb. Olduğu karma tabloları kullanan bir projeye başladım. Asal sayı boyutlarını tercih etmek için iyi bir neden var, ama orada iki boyutun gücüne de bazı avantajlar sağlar.
Ve hemen hemen emdi. Biraz deneme ve araştırma yaptıktan sonra hashlerimi yeniden hash etmeye başladım:
public static int ReHash(int source)
{
unchecked
{
ulong c = 0xDEADBEEFDEADBEEF + (ulong)source;
ulong d = 0xE2ADBEEFDEADBEEF ^ c;
ulong a = d += c = c << 15 | c >> -15;
ulong b = a += d = d << 52 | d >> -52;
c ^= b += a = a << 26 | a >> -26;
d ^= c += b = b << 51 | b >> -51;
a ^= d += c = c << 28 | c >> -28;
b ^= a += d = d << 9 | d >> -9;
c ^= b += a = a << 47 | a >> -47;
d ^= c += b << 54 | b >> -54;
a ^= d += c << 32 | c >> 32;
a += d << 25 | d >> -25;
return (int)(a >> 1);
}
}
Ve sonra iki kişilik karma masam artık emmedi.
Bu beni rahatsız etti, çünkü yukarıdakiler işe yaramamalı. Ya da daha doğrusu, orijinal GetHashCode()
çok belirli bir şekilde fakir değilse, çalışmamalıdır .
Bir karma kodun yeniden karıştırılması büyük bir karma kodu iyileştiremez, çünkü olası tek etki birkaç çarpışma daha getirmemizdir.
Bir karma kodunun yeniden karıştırılması korkunç bir karma kodunu iyileştiremez, çünkü olası tek etki örneğin 53 değeri üzerindeki çok sayıda çarpışmayı çok sayıda 18.3487.291 değerine değiştirmektir.
Bir karma kodunun yeniden karıştırılması, aralığı boyunca mutlak çarpışmalardan kaçınmak için en azından oldukça iyi bir karma kodunu iyileştirebilir (2 32 olası değer), ancak bir karma tablosunda gerçek kullanım için modüle edildiğinde çarpışmalardan kaçınmak için kötü bir şekilde. İki güçten oluşan bir tablonun daha basit modulou bunu daha belirgin hale getirirken, daha yaygın olan asal sayı tablolarıyla da olumsuz bir etkisi vardı, ki bu çok açık değildi (yeniden şekillendirmedeki ekstra çalışma faydadan daha ağır basacaktı , ancak fayda hala orada olurdu).
Düzenleme: Ben de çarpışma duyarlılığını artıracak açık adresleme kullanıyordum, belki de iki güç olduğu gerçeğinden daha fazla.
Ve de, ne kadar rahatsız oldu string.GetHashCode()
uygulamalarda .NET (veya çalışma burada (nedeniyle daha az çarpışmaları daha hızlı 20-30 kere çalışan testlerin sipariş üzerine) bu şekilde geliştirilebilir) ve daha ne kadar kendi karma kodlar rahatsız geliştirilebilir (bundan çok daha fazlası).
Geçmişte kodladığım ve aslında bu sitedeki cevapların temeli olarak kullandığım tüm GetHashCode () uygulamaları, düşündüğümden çok daha kötüydü . Çoğu zaman bu kullanımlar için "yeterince iyi", ama daha iyi bir şey istedim.
Bu yüzden bu projeyi bir tarafa koydum (yine de bir evcil hayvan projesiydi) ve hızlı bir şekilde .NET'te iyi, iyi dağıtılmış bir karma kodun nasıl üretileceğini araştırmaya başladım.
Sonunda SpookyHash'i .NET'e taşımaya karar verdim . Aslında yukarıdaki kod, 32 bit girişten 32 bit çıkış üretmek için SpookyHash kullanmanın hızlı yollu bir sürümüdür.
Şimdi, SpookyHash bir kod parçasını hatırlamak hoş bir hızlı değil. Bağlantı noktam daha da azdır, çünkü daha iyi hız için çok fazla elle satır çizdim *. Ancak bu kodun yeniden kullanımı içindir.
Sonra bu projeyi bir tarafa koydum , çünkü orijinal projenin nasıl daha iyi bir karma kod üretileceği sorusunu ürettiği gibi, bu proje daha iyi bir .NET memcpy'nin nasıl üretileceği sorusunu üretti.
Sonra geri döndüm ve hemen hemen tüm yerli türleri ( decimal
† hariç ) bir karma koduna kolayca beslemek için aşırı yükler ürettim.
Bob Jenkins kredinin çoğunu hak ettiği için hızlı, çünkü taşıdığım orijinal kodu daha hızlı, özellikle de algoritmanın ‡ için optimize edildiği 64 bit makinelerde daha hızlı.
Kodun tamamı https://bitbucket.org/JonHanna/spookilysharp/src adresinde görülebilir, ancak yukarıdaki kodun basitleştirilmiş bir versiyonu olduğunu düşünün.
Ancak, daha önce yazıldığı için bundan daha kolay yararlanılabilir:
public override int GetHashCode()
{
var hash = new SpookyHash();
hash.Update(field1);
hash.Update(field2);
hash.Update(field3);
return hash.Final().GetHashCode();
}
Ayrıca tohum değerleri de alır, bu nedenle güvenilmeyen girdilerle uğraşmanız ve Hash DoS saldırılarına karşı korunmanız gerekiyorsa, çalışma süresine veya benzerine dayalı bir tohum ayarlayabilir ve sonuçları saldırganlar tarafından öngörülemez hale getirebilirsiniz:
private static long hashSeed0 = Environment.TickCount;
private static long hashSeed1 = DateTime.Now.Ticks;
public override int GetHashCode()
{
//produce different hashes ever time this application is restarted
//but remain consistent in each run, so attackers have a harder time
//DoSing the hash tables.
var hash = new SpookyHash(hashSeed0, hashSeed1);
hash.Update(field1);
hash.Update(field2);
hash.Update(field3);
return hash.Final().GetHashCode();
}
* Buradaki büyük sürpriz, (x << n) | (x >> -n)
iyileştirilmiş şeyleri döndüren bir rotasyon yönteminin elle yerleştirilmesidir . Jitterin bunu benim için sıraya koyacağından emin olurdum, ancak profilleme aksini gösterdi.
† decimal
, C # 'dan olmasına rağmen .NET açısından yerel değildir. Sorun şu ki, kendi GetHashCode()
hassasiyeti önemli olarak dikkate alırken, kendi başına duyarlı Equals()
değildir. Her ikisi de geçerli seçimlerdir, ancak böyle karıştırılmamıştır. Kendi sürümünüzü uygularken, birini veya diğerini seçmeniz gerekir, ancak hangisini istediğinizi bilemiyorum.
Comparison Karşılaştırma yoluyla. Bir dize üzerinde kullanılırsa, 64 bit üzerindeki SpookyHash, string.GetHashCode()
32 bit üzerinde biraz daha hızlıdır string.GetHashCode()
, bu da 64 bitten biraz daha hızlıdır , bu da 32 bit üzerindeki SpookyHash'ten oldukça hızlıdır, ancak yine de makul bir seçim olacak kadar hızlıdır.
GetHashCode
. Umarım başkaları için yararlı olur. Eric Lippert tarafından yazılmış GetHashCode için kurallar ve kurallar