Cho đến gần đây, câu trả lời của tôi rất gần với Jon Skeet ở đây. Tuy nhiên, gần đây tôi đã bắt đầu một dự án sử dụng các bảng băm có hai sức mạnh, đó là các bảng băm trong đó kích thước của bảng nội bộ là 8, 16, 32, v.v ... Có một lý do chính đáng để ưu tiên kích thước số nguyên tố, nhưng có là một số lợi thế cho sức mạnh của hai kích thước quá.
Và nó bị hút khá nhiều. Vì vậy, sau một chút thử nghiệm và nghiên cứu, tôi bắt đầu băm lại băm của mình bằng cách sau:
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);
}
}
Và rồi bảng băm hai sức mạnh của tôi không còn hút nữa.
Điều này làm tôi băn khoăn, vì những điều trên không nên làm việc. Hay chính xác hơn, nó không hoạt động trừ khi bản gốc GetHashCode()
kém theo một cách rất riêng.
Trộn lại mã băm không thể cải thiện mã băm tuyệt vời, bởi vì hiệu quả duy nhất có thể là chúng tôi giới thiệu thêm một vài va chạm.
Trộn lại mã băm không thể cải thiện mã băm khủng khiếp, bởi vì hiệu ứng duy nhất có thể là chúng ta thay đổi, ví dụ như một số lượng lớn các va chạm trên giá trị 53 thành một số lượng lớn giá trị 18.3487.291.
Trộn lại mã băm chỉ có thể cải thiện mã băm ít nhất là khá tốt trong việc tránh va chạm tuyệt đối trong phạm vi của nó (2 32 giá trị có thể) nhưng rất tệ trong việc tránh va chạm khi modulo xuống sử dụng thực tế trong bảng băm. Mặc dù mô-đun đơn giản hơn của bảng hai sức mạnh làm cho điều này rõ ràng hơn, nhưng nó cũng có tác động tiêu cực với các bảng số nguyên tố phổ biến hơn, điều đó không rõ ràng (công việc bổ sung trong việc luyện lại sẽ vượt trội hơn lợi ích , nhưng lợi ích vẫn còn đó).
Chỉnh sửa: Tôi cũng đang sử dụng địa chỉ mở, điều này cũng sẽ làm tăng độ nhạy cảm với va chạm, có lẽ nhiều hơn thực tế là nó có sức mạnh hai.
Và tốt, điều đáng lo ngại là string.GetHashCode()
việc triển khai .NET (hoặc nghiên cứu ở đây ) có thể được cải thiện theo cách này (theo thứ tự các bài kiểm tra chạy nhanh hơn khoảng 20-30 lần do ít va chạm hơn) và làm phiền nhiều hơn bao nhiêu mã băm của riêng tôi có thể được cải thiện (nhiều hơn thế).
Tất cả các triển khai GetHashCode () mà tôi đã mã hóa trong quá khứ và thực sự được sử dụng làm cơ sở của các câu trả lời trên trang web này, tồi tệ hơn nhiều so với trước đây . Phần lớn thời gian là "đủ tốt" cho phần lớn mục đích sử dụng, nhưng tôi muốn thứ gì đó tốt hơn.
Vì vậy, tôi đặt dự án đó sang một bên (dù sao đó cũng là một dự án thú cưng) và bắt đầu xem xét cách tạo ra mã băm tốt, được phân phối tốt trong .NET một cách nhanh chóng.
Cuối cùng, tôi quyết định chuyển SpookyHash sang .NET. Thật vậy, đoạn mã trên là phiên bản đường dẫn nhanh của việc sử dụng SpookyHash để tạo đầu ra 32 bit từ đầu vào 32 bit.
Bây giờ, SpookyHash không phải là một đoạn mã nhanh để nhớ. Cổng của tôi thậm chí còn ít hơn bởi vì tôi đã nhúng tay rất nhiều để có tốc độ tốt hơn *. Nhưng đó là những gì mã tái sử dụng là dành cho.
Sau đó, tôi đặt dự án đó sang một bên, vì giống như dự án ban đầu đã tạo ra câu hỏi làm thế nào để tạo ra mã băm tốt hơn, để dự án đó tạo ra câu hỏi về cách sản xuất một memcpy .NET tốt hơn.
Sau đó, tôi trở lại và tạo ra rất nhiều tình trạng quá tải để dễ dàng cung cấp tất cả các loại bản địa (trừ decimal
†) vào mã băm.
Thật nhanh, vì Bob Jenkins xứng đáng nhận phần lớn tín dụng vì mã gốc mà tôi chuyển đến vẫn nhanh hơn, đặc biệt là trên các máy 64 bit mà thuật toán được tối ưu hóa cho.
Mã đầy đủ có thể được xem tại https://bitbucket.org/JonHanna/spookilysharp/src nhưng xem xét rằng mã ở trên là phiên bản đơn giản hóa của nó.
Tuy nhiên, vì hiện tại nó đã được viết, người ta có thể sử dụng nó dễ dàng hơn:
public override int GetHashCode()
{
var hash = new SpookyHash();
hash.Update(field1);
hash.Update(field2);
hash.Update(field3);
return hash.Final().GetHashCode();
}
Nó cũng lấy các giá trị hạt giống, vì vậy nếu bạn cần xử lý đầu vào không đáng tin cậy và muốn bảo vệ chống lại các cuộc tấn công Hash DoS, bạn có thể đặt hạt giống dựa trên thời gian hoạt động hoặc tương tự, và làm cho kết quả không thể đoán trước bởi những kẻ tấn công:
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();
}
* Một bất ngờ lớn ở đây là việc đưa ra một phương pháp xoay vòng đã trả lại (x << n) | (x >> -n)
những thứ được cải thiện. Tôi đã chắc chắn rằng jitter sẽ đưa ra điều đó cho tôi, nhưng hồ sơ cho thấy khác.
† decimal
là không có nguồn gốc từ quan điểm NET mặc dù nó là từ C #. Vấn đề với nó là chính nó GetHashCode()
coi độ chính xác là quan trọng trong khi chính nó Equals()
thì không. Cả hai đều là lựa chọn hợp lệ, nhưng không trộn lẫn như thế. Khi thực hiện phiên bản của riêng bạn, bạn cần chọn thực hiện cái này hoặc cái kia, nhưng tôi không thể biết bạn muốn cái nào.
Bằng cách so sánh. Nếu được sử dụng trên một chuỗi, SpookyHash trên 64 bit nhanh hơn đáng kể so với string.GetHashCode()
trên 32 bit, nhanh hơn một chút so với string.GetHashCode()
trên 64 bit, nhanh hơn đáng kể so với SpookyHash trên 32 bit, mặc dù vẫn đủ nhanh để là một lựa chọn hợp lý.
GetHashCode
. Tôi hy vọng nó sẽ hữu ích cho những người khác. Nguyên tắc và quy tắc cho GetHashCode được viết bởi Eric Lippert