En .NET, el GetHashCode
método se usa en muchos lugares de las bibliotecas de clases base .NET. Implementarlo adecuadamente es especialmente importante para encontrar elementos rápidamente en una colección o al determinar la igualdad.
¿Existe un algoritmo estándar o una mejor práctica sobre cómo implementar GetHashCode
mis clases personalizadas para no degradar el rendimiento?
.net
algorithm
hashcode
gethashcode
bitbonk
fuente
fuente
GetHashCode
. Espero que sea útil para otros. Pautas y reglas para GetHashCode escrito por Eric LippertGetHashCode()
se usa en muchas implementaciones deEquals()
. Eso es lo que quise decir con esa declaración.GetHashCode()
adentro aEquals()
menudo se usa como un atajo para determinar la desigualdad , porque si dos objetos tienen un código hash diferente , deben ser objetos que no son iguales y el resto de la verificación de igualdad no tiene que ejecutarse.GetHashCode()
yEquals()
necesitan mirar todos los campos de ambos objetos (Equals tiene que hacer esto si los códigos hash son iguales o no están marcados). Debido a esto, una llamada alGetHashCode()
interior aEquals()
menudo es redundante y podría reducir el rendimiento.Equals()
también puede ser capaz de provocar un cortocircuito, lo que lo hace mucho más rápido; sin embargo, en algunos casos, los códigos hash pueden almacenarse en caché, lo que hace que laGetHashCode()
verificación sea más rápida y valga la pena. Vea esta pregunta para más.Respuestas:
Por lo general, uso algo como la implementación dada en el fabuloso Java eficaz de Josh Bloch . Es rápido y crea un hash bastante bueno que es poco probable que cause colisiones. Elija dos números primos diferentes, por ejemplo, 17 y 23, y haga:
Como se señaló en los comentarios, es posible que sea mejor elegir una prima grande para multiplicar en su lugar. Aparentemente, 486187739 es bueno ... y aunque la mayoría de los ejemplos que he visto con números pequeños tienden a usar números primos, existen al menos algoritmos similares en los que a menudo se usan números no primos. En el ejemplo no- FNV más adelante, por ejemplo, he usado números que aparentemente funcionan bien, pero el valor inicial no es primo. (Sin embargo, la constante de multiplicación es primo. No sé qué tan importante es eso).
Esto es mejor que la práctica común de
XOR
codificar hash por dos razones principales. Supongamos que tenemos un tipo con dosint
campos:Por cierto, el algoritmo anterior es el utilizado actualmente por el compilador de C # para tipos anónimos.
Esta página ofrece bastantes opciones. Creo que para la mayoría de los casos lo anterior es "suficientemente bueno" y es increíblemente fácil de recordar y acertar. La alternativa FNV es similarmente simple, pero usa diferentes constantes y en
XOR
lugar deADD
una operación combinada. Se ve algo como el código de abajo, pero el algoritmo FNV normales opera en bytes individuales, por lo que esto requeriría la modificación de realizar una iteración por byte, en lugar de por valor hash de 32 bits. FNV también está diseñado para longitudes variables de datos, mientras que la forma en que lo estamos usando aquí es siempre para el mismo número de valores de campo. Los comentarios sobre esta respuesta sugieren que el código aquí en realidad no funciona tan bien (en el caso de muestra probado) como el enfoque de adición anterior.Tenga en cuenta que una cosa a tener en cuenta es que, idealmente, debe evitar que su estado sensible a la igualdad (y, por lo tanto, sensible al código hash) cambie después de agregarlo a una colección que depende del código hash.
Según la documentación :
fuente
Dictionary<TKey,TValue>
supone una buena distribución del módulo de ciertos primos. Y 23 es uno de ellos. Entonces, si tiene un diccionario con Capacidad 23, solo la última contribuciónGetHashCode
influye en el código hash compuesto. Así que prefiero usar 29 en lugar de 23.null
, lo cual no es lo mismo que ignorar el campo.Tipo anónimo
Microsoft ya proporciona un buen generador genérico de HashCode: simplemente copie sus valores de propiedad / campo a un tipo anónimo y diviértalo:
Esto funcionará para cualquier número de propiedades. No usa boxeo. Simplemente usa el algoritmo ya implementado en el marco para tipos anónimos.
ValueTuple - Actualización para C # 7
Como @cactuaroid menciona en los comentarios, se puede usar una tupla de valor. Esto ahorra algunas pulsaciones de teclas y, lo que es más importante, se ejecuta únicamente en la pila (sin basura):
(Nota: la técnica original que usa tipos anónimos parece crear un objeto en el montón, es decir, basura, ya que los tipos anónimos se implementan como clases, aunque esto podría ser optimizado por el compilador. Sería interesante comparar estas opciones, pero La opción de tupla debe ser superior.)
fuente
GetHashCode
implementación anónima es muy efectiva (por cierto, es la misma que la respuesta de Jon Skeet), pero el único problema con esta solución es que genera una nueva instancia en cualquierGetHashCode
llamada. Puede ser un poco excesivo, en particular en caso de acceso intensivo a grandes colecciones hash ...new { PropA, PropB, PropC, PropD }.GetHashCode()
tambiénNew With {Key PropA}.GetHashCode()
contrario, GetHashCode no devolverá el mismo código hash para diferentes objetos con las mismas propiedades de "identificación".Aquí está mi ayudante de hashcode.
Su ventaja es que usa argumentos de tipo genérico y, por lo tanto, no causará boxeo:
También tiene un método de extensión para proporcionar una interfaz fluida, por lo que puede usarlo así:
o así:
fuente
T[]
separado, ya que esIEnumerable<T>
Tengo una clase de Hashing en la biblioteca auxiliar que la uso para este propósito.
Entonces, simplemente puedes usarlo como:
No evalué su rendimiento, por lo que cualquier comentario es bienvenido.
fuente
unchecked
Excepción de desbordamiento " El objetivo principal es evitar las excepciones de desbordamiento que se desean enGetHashCode
. Por lo tanto, no es incorrecto si el valor se desbordaint
y no duele en absoluto.null
se omita por completo podría brindarle resultados inesperados. En lugar de omitirlos, debe usar un valor constante en lugar deinput[i].GetHashCode()
cuándoinput[i]
es nulo.Aquí está mi clase de ayuda usando la implementación de Jon Skeet .
Uso:
Si desea evitar escribir un método de extensión para System.Int32:
Todavía evita cualquier asignación de montón y se usa exactamente de la misma manera:
Editar (mayo de 2018):
EqualityComparer<T>.Default
getter ahora es un JIT intrínseco: Stephen Toub menciona la solicitud de extracción en esta publicación de blog .fuente
var h = Equals(obj, default(T)) ? 0 : obj.GetHashCode();
obj != null
compilará a unabox
instrucción que asignará memoria siT
es un tipo de valor. En su lugar, puede usar elobj.Equals(null)
que se compilará en una llamada virtual delEquals
método.this.hashCode != h
. No devolvería el mismo valor..NET Standard 2.1 y superior
Si está utilizando .NET Standard 2.1 o superior, puede usar la estructura System.HashCode . Hay dos métodos para usarlo:
HashCode.Combine
El
Combine
método puede usarse para crear un código hash, dado hasta ocho objetos.HashCode.Add
El
Add
método te ayuda a lidiar con las colecciones:GetHashCode Made Easy
Puede leer la publicación completa del blog ' GetHashCode Made Easy ' para obtener más detalles y comentarios.
Ejemplo de uso
Implementación
¿Qué hace un buen algoritmo?
Velocidad
El algoritmo que calcula un código hash debe ser rápido. Un algoritmo simple generalmente será más rápido.
Determinista
El algoritmo de hashing debe ser determinista, es decir, dada la misma entrada, siempre debe producir la misma salida.
Reducir colisiones
El algoritmo que calcula un código hash necesita mantener las colisiones hash a un mínimo. Una colisión hash es una situación que ocurre cuando dos llamadas a
GetHashCode
dos objetos diferentes producen códigos hash idénticos. Tenga en cuenta que las colisiones están permitidas (algunas tienen la idea errónea de que no lo están), pero deben mantenerse al mínimo.Una buena función hash debe asignar las entradas esperadas de la manera más uniforme posible en su rango de salida. Debe tener uniformidad.
Prevenir DoS
En .NET Core cada vez que reinicia una aplicación obtendrá diferentes códigos hash. Esta es una característica de seguridad para evitar ataques de denegación de servicio (DoS). Para .NET Framework, debe habilitar esta característica agregando el siguiente archivo App.config:
Debido a esta característica, los códigos hash nunca deben usarse fuera del dominio de aplicación en el que fueron creados, nunca deben usarse como campos clave en una colección y nunca deben persistirse.
Lea más sobre esto aquí .
¿Criptográficamente seguro?
El algoritmo no tiene que ser una función hash criptográfica . Lo que significa que no tiene que cumplir las siguientes condiciones:
fuente
En la mayoría de los casos en los que Equals () compara múltiples campos, realmente no importa si su GetHash () tiene hash en un campo o en muchos. Solo tiene que asegurarse de que calcular el hash sea realmente barato ( sin asignaciones , por favor) y rápido ( sin cálculos pesados y ciertamente sin conexiones de base de datos) y que proporcione una buena distribución.
El trabajo pesado debe ser parte del método Equals (); el hash debería ser una operación muy barata para permitir llamar a Equals () en la menor cantidad de elementos posible.
Y un consejo final: no confíe en que GetHashCode () sea estable en múltiples ejecuciones de aplicaciones . Muchos tipos .Net no garantizan que sus códigos hash permanezcan igual después de un reinicio, por lo que solo debe usar el valor de GetHashCode () para estructuras de datos en memoria.
fuente
GetHashCode
realizar asignaciones de memoria, siempre que solo lo haga la primera vez que se use (con invocaciones posteriores que simplemente devuelven un resultado en caché). Lo importante no es que uno deba hacer grandes esfuerzos para evitar colisiones, sino que debe evitar colisiones "sistémicas". Si un tipo tiene dosint
camposoldX
y connewX
frecuencia difieren en uno, un valor hash deoldX^newX
asignaría el 90% de dichos valores hash de registros de 1, 2, 4 u 8. El uso deoldX+newX
[aritmética no verificada] podría generar más colisiones ...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:
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:
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:
* 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 propiaGetHashCode()
trata la precisión como significativa, mientras que la suyaEquals()
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 questring.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.fuente
long
valores para los resultados intermedios y luego reducir el resultado final a unint
. ¿Te parece una buena idea? Mi preocupación es que uno usa, por ejemplo, hash = (hash * 31) + nextField, luego los pares de valores coincidentes solo afectarán los 27 bits superiores del hash. Dejar que el cálculo se extienda a unalong
y envolver las cosas minimizaría ese peligro..Update()
con los múltiples valores según la respuesta anterior hará el truco.Este es bueno:
Y aquí está cómo usarlo:
fuente
GetHashCode()
método, por lo que siempre puede usar el método con elparams
parámetro de matriz. ¿O me estoy perdiendo algo aquí?h += (h << 10); h ^= (h >> 6); h += (h << 3); h ^= (h >> 11); h += (h << 15);
tienen una hediondez del código: no dependen de ninguna de la entrada y mirar muy redundante para mí.A partir de https://github.com/dotnet/coreclr/pull/14863 , ¡hay una nueva forma de generar códigos hash que es súper simple! Solo escribe
Esto generará un código hash de calidad sin que tenga que preocuparse por los detalles de implementación.
fuente
HashCode
cambios para corefx se fusionaron solo un par de horas antes de tu comentario :) El tipo está programado para enviarse en .NET Core 2.1.Aquí hay otra implementación fluida del algoritmo publicado anteriormente por Jon Skeet , pero que no incluye asignaciones ni operaciones de boxeo:
Uso:
El compilador se asegurará de
HashValue
que no se llame con una clase debido a la restricción de tipo genérico. Pero no hay soporte para el compiladorHashObject
ya que agregar un argumento genérico también agrega una operación de boxeo.fuente
Aquí está mi enfoque simplista. Estoy usando el patrón de construcción clásico para esto. Es de tipo seguro (sin boxing / unboxing) y también compatible con .NET 2.0 (sin métodos de extensión, etc.).
Se usa así:
Y aquí está la clase de constructor acutal:
fuente
AddItems<T>(params T[] items)
método con más frecuencia en la clase auxiliar (que llamarAddItem(T)
cada vez).this.result * Prime2 * item.GetHashCode()
cuando se usa con frecuenciathis.result * Prime2 + item.GetHashCode()
?AddItems<T>(params T[] items)
más a menudo porquetypeof(T1) != typeof(T2)
etc.Los usuarios de ReSharper pueden generar GetHashCode, Equals y otros con
ReSharper -> Edit -> Generate Code -> Equality Members
.fuente
Si no tenemos más de 8 propiedades (con suerte), aquí hay otra alternativa.
ValueTuple
es una estructura y parece tener unaGetHashCode
implementación sólida .Eso significa que simplemente podríamos hacer esto:
Vamos a echar un vistazo a la aplicación actual de .NET Core de
ValueTuple
'sGetHashCode
.Esto es de
ValueTuple
:Y esto es de
HashHelper
:En inglés:
Sería bueno saber más sobre las propiedades de este algoritmo de código hash ROL-5.
Lamentablemente, diferir
ValueTuple
para los nuestrosGetHashCode
puede no ser tan rápido como nos gustaría y esperar. Este comentario en una discusión relacionada ilustra que llamar directamenteHashHelpers.Combine
es más eficiente. Por otro lado, ese es interno, por lo que tendríamos que copiar el código, sacrificando gran parte de lo que habíamos ganado aquí. Además, seríamos responsables de recordar primeroCombine
con la semilla aleatoria. No sé cuáles son las consecuencias si omitimos ese paso.fuente
h1 >> 27
es 0 para ignorarlo,h1 << 5
es igual ,h1 * 32
por lo tanto, es igual queh1 * 33 ^ h2
. Según esta página , se llama "Bernstein modificado".La mayor parte de mi trabajo se realiza con la conectividad de la base de datos, lo que significa que todas mis clases tienen un identificador único de la base de datos. Siempre uso el ID de la base de datos para generar el código hash.
fuente
_id.GetHashCode
ya que la intención es clara.Bastante similar a la solución del codificador nocturno, excepto que es más fácil aumentar los números primos si lo desea.
PD: Esta es una de esas veces en las que vomitas un poco en la boca, sabiendo que esto podría ser refactorizado en un método con 9 valores predeterminados, pero sería más lento, por lo que solo cierra los ojos y trata de olvidarte.
fuente
Me encontré con un problema con flotantes y decimales usando la implementación seleccionada como la respuesta anterior.
Esta prueba falla (flota; el hash es el mismo aunque cambié 2 valores a negativo):
Pero esta prueba pasa (con ints):
Cambié mi implementación para no usar GetHashCode para los tipos primitivos y parece funcionar mejor
fuente
unchecked
NO afectaConvert.ToInt32
:uint
,long
,float
,double
ydecimal
pueden todos desbordamiento aquí.Microsoft lidera varias formas de hash ...
Puedo adivinar que para múltiples big int puedes usar esto:
Y lo mismo para el tipo múltiple: todos se convierten primero en
int
usar,GetHashCode()
luego los valores int se corregirán y el resultado será su hash.Para aquellos que usan hash como ID (me refiero a un valor único), el hash está naturalmente limitado a un número de dígitos, creo que fueron 5 bytes para el algoritmo de hash, al menos MD5.
Puede convertir varios valores en un valor hash y algunos de ellos son iguales, así que no lo use como identificador. (tal vez algún día voy a usar su componente)
fuente
Esta es una clase auxiliar estática que implementa la implementación de Josh Bloch; y proporciona sobrecargas explícitas para "prevenir" el boxeo, y también para implementar el hash específicamente para las primitivas largas.
Puede pasar una comparación de cadenas que coincida con su implementación igual.
Debido a que la salida de Hash siempre es un int, puede simplemente encadenar llamadas de Hash.
fuente
HashKeysAndValues
método ha sido arreglado: invocaHashKeyAndValue
.En caso de que quiera rellenar
HashCode
desdenetstandard2.1
Nota: Si se usa con
struct
, asignará memoria debido al boxeofuente