Quería almacenar algunas ubicaciones de píxeles sin permitir duplicados, por lo que lo primero que me viene a la mente son las HashSet<Point>clases similares. Sin embargo, esto parece ser muy lento en comparación con algo así HashSet<string>.
Por ejemplo, este código:
HashSet<Point> points = new HashSet<Point>();
using (Bitmap img = new Bitmap(1000, 1000))
{
for (int x = 0; x < img.Width; x++)
{
for (int y = 0; y < img.Height; y++)
{
points.Add(new Point(x, y));
}
}
}
Toma alrededor de 22.5 segundos.
Si bien el siguiente código (que no es una buena opción por razones obvias) solo lleva 1.6 segundos:
HashSet<string> points = new HashSet<string>();
using (Bitmap img = new Bitmap(1000, 1000))
{
for (int x = 0; x < img.Width; x++)
{
for (int y = 0; y < img.Height; y++)
{
points.Add(x + "," + y);
}
}
}
Entonces, mis preguntas son:
- ¿Hay alguna razón para eso? Verifiqué esta respuesta , pero 22.5 segundos es mucho más que los números que se muestran en esa respuesta.
- ¿Hay una mejor manera de almacenar puntos sin duplicados?
c#
.net
performance
collections
hashset
Ahmed Abdelhameed
fuente
fuente

Respuestas:
Hay dos problemas de rendimiento inducidos por la estructura Point. Algo que puede ver cuando agrega
Console.WriteLine(GC.CollectionCount(0));al código de prueba. Verá que la prueba Point requiere ~ 3720 colecciones, pero la prueba de cadena solo necesita ~ 18 colecciones. No gratis. Cuando veas que un tipo de valor induce tantas colecciones, entonces debes concluir "uh-oh, demasiado boxeo".El problema es que
HashSet<T>necesita unIEqualityComparer<T>para hacer su trabajo. Como no proporcionó uno, debe recurrir a uno devuelto porEqualityComparer.Default<T>(). Ese método puede hacer un buen trabajo para la cadena, implementa IEquatable. Pero no para Point, es un tipo que se inspira en .NET 1.0 y nunca tuvo el amor genérico. Todo lo que puede hacer es usar los métodos Object.El otro problema es que Point.GetHashCode () no hace un trabajo estelar en esta prueba, demasiadas colisiones, por lo que golpea Object.Equals () bastante fuertemente. String tiene una excelente implementación de GetHashCode.
Puede resolver ambos problemas proporcionando al HashSet un buen comparador. Como éste:
Y úsalo:
Y ahora es aproximadamente 150 veces más rápido, superando fácilmente la prueba de cuerda.
fuente
obj.X << 16 | obj.Y;implementación particular ?|. Para 3 números, podría tener sentido usar 22 y 11 como desplazamiento. Para 4 números, sería 24, 16, 8. Sin embargo, todavía habrá colisiones, pero solo si los números aumentan. Pero también depende de manera crucial de laHashSetimplementación. Si usa direccionamiento abierto con "truncamiento de bits" (¡no creo que lo haga!), El enfoque de desplazamiento a la izquierda podría ser malo.La razón principal de la caída del rendimiento es todo el boxeo (como ya se explicó en la respuesta de Hans Passant ).
Además de eso, el algoritmo de código hash empeora el problema, ya que provoca más llamadas para
Equals(object obj)aumentar así la cantidad de conversiones de boxeo.También tenga en cuenta que el código hash de
Pointes calculado porx ^ y. Esto produce muy poca dispersión en su rango de datos y, por lo tanto, los cubosHashSetestán superpoblados, algo con lo que no sucedestring, donde la dispersión de los hashes es mucho mayor.Puede resolver ese problema implementando su propia
Pointestructura (trivial) y utilizando un mejor algoritmo hash para su rango de datos esperado, por ejemplo, cambiando las coordenadas:Para algunos buenos consejos cuando se trata de códigos hash, lea la publicación de blog de Eric Lippert sobre el tema .
fuente
GetHashCoderealiza:unchecked(x ^ y)mientras que parastringparece mucho más complicado ..HashSet<long>()lugar ylist.Add(unchecked(x ^ y));agregué valores al HashSet. En realidad, esto fue incluso más rápido queHashSet<string>(345 ms) . ¿Es esto de alguna manera diferente de lo que describiste?listcuando hayas terminado de poblarlo?point,HashSetse llamará internamenteGetHashCodey para cada uno de esos puntos con el mismo código hash, se llamaráEqualspara determinar si ya existePointcuando puede crear una clase que implementeIEqualityComparer<Point>y mantenga la compatibilidad con otras cosas con las que trabajaPointmientras obtiene el beneficio de no tener a los pobresGetHashCodey la necesidad de encajonarEquals().