Hasta hace poco, mi respuesta habría estado muy cerca de la de Jon Skeet. Sin embargo, recientemente comencé un proyecto que usaba tablas hash de potencia de dos, es decir tablas hash donde el tamaño de la tabla interna es 8, 16, 32, etc. Hay una buena razón para favorecer los tamaños de números primos, pero hay También hay algunas ventajas para los tamaños de potencia de dos.
Y casi apestaba. Entonces, después de un poco de experimentación e investigación, comencé a volver a mezclar mis hash con lo siguiente:
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);
}
}
Y luego mi tabla hash de poder de dos ya no apestaba.
Sin embargo, esto me molestó, porque lo anterior no debería funcionar. O más precisamente, no debería funcionar a menos que el original GetHashCode()
fuera pobre de una manera muy particular.
Volver a mezclar un código hash no puede mejorar un gran código hash, porque el único efecto posible es que introducimos algunas colisiones más.
Volver a mezclar un código hash no puede mejorar un terrible código hash, porque el único efecto posible es que cambiemos, por ejemplo, una gran cantidad de colisiones en el valor 53 a una gran cantidad de valor 18,3487,291.
Remezclar un código hash solo puede mejorar un código hash que funcionó al menos bastante bien para evitar colisiones absolutas en todo su rango (2 32 valores posibles) pero mal para evitar colisiones cuando el módulo está inactivo para uso real en una tabla hash. Si bien el módulo más simple de una tabla de potencia de dos lo hizo más evidente, también estaba teniendo un efecto negativo con las tablas de números primos más comunes, eso no era tan obvio (el trabajo adicional en la repetición superaría el beneficio , pero el beneficio aún estaría allí).
Editar: también estaba usando direccionamiento abierto, lo que también habría aumentado la sensibilidad a la colisión, tal vez más que el hecho de que era poder de dos.
Y bueno, fue inquietante cuánto podrían mejorarse las string.GetHashCode()
implementaciones en .NET (o estudio aquí ) de esta manera (en el orden de las pruebas que se ejecutan entre 20 y 30 veces más rápido debido a menos colisiones) y más inquietante cuánto mis propios códigos hash podría mejorarse (mucho más que eso).
Todas las implementaciones de GetHashCode () que codifiqué en el pasado, y que de hecho utilicé como la base de las respuestas en este sitio, fueron mucho peores de lo que lo había hecho . La mayor parte del tiempo fue "lo suficientemente bueno" para muchos de los usos, pero quería algo mejor.
Así que puse ese proyecto a un lado (de todos modos era un proyecto favorito) y comencé a buscar cómo producir un código hash bueno y bien distribuido en .NET rápidamente.
Al final me decidí a portar SpookyHash a .NET. De hecho, el código anterior es una versión de ruta rápida del uso de SpookyHash para producir una salida de 32 bits a partir de una entrada de 32 bits.
Ahora, SpookyHash no es un buen código rápido para recordar. Mi puerto es aún menos porque lo he insertado a mano para una mejor velocidad *. Pero para eso está la reutilización de código.
Luego puse ese proyecto a un lado, porque así como el proyecto original había producido la pregunta de cómo producir un mejor código hash, ese proyecto produjo la pregunta de cómo producir una mejor memoria .NET.
Luego regresé y produje muchas sobrecargas para alimentar fácilmente casi todos los tipos nativos (excepto decimal
†) en un código hash.
Es rápido, por lo que Bob Jenkins merece la mayor parte del crédito porque su código original del que lo porté es aún más rápido, especialmente en máquinas de 64 bits para las cuales el algoritmo está optimizado ‡.
El código completo se puede ver en https://bitbucket.org/JonHanna/spookilysharp/src, pero considere que el código anterior es una versión simplificada.
Sin embargo, dado que ahora ya está escrito, uno puede usarlo más fácilmente:
public override int GetHashCode()
{
var hash = new SpookyHash();
hash.Update(field1);
hash.Update(field2);
hash.Update(field3);
return hash.Final().GetHashCode();
}
También toma valores iniciales, por lo que si necesita lidiar con datos no confiables y desea protegerse contra los ataques Hash DoS, puede establecer una semilla basada en el tiempo de actividad o similar, y hacer que los resultados sean impredecibles para los atacantes:
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();
}
* Una gran sorpresa en esto es que incluyó a mano un método de rotación que devolvió (x << n) | (x >> -n)
cosas mejoradas. Habría estado seguro de que la inquietud me lo habría explicado, pero el perfil mostró lo contrario.
† decimal
no es nativo desde la perspectiva .NET, aunque sí lo es desde C #. El problema con esto es que su propia GetHashCode()
trata la precisión como significativa, mientras que la suya Equals()
no lo hace. Ambas son opciones válidas, pero no se mezclan así. Al implementar su propia versión, debe elegir hacer una u otra, pero no puedo saber cuál le gustaría.
‡ A modo de comparación. Si se usa en una cadena, SpookyHash en 64 bits es considerablemente más rápido que string.GetHashCode()
en 32 bits, que es ligeramente más rápido que string.GetHashCode()
en 64 bits, que es considerablemente más rápido que SpookyHash en 32 bits, aunque aún lo suficientemente rápido como para ser una elección razonable.
GetHashCode
. Espero que sea útil para otros. Pautas y reglas para GetHashCode escrito por Eric Lippert