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 laHashSet
implementació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
Point
es calculado porx ^ y
. Esto produce muy poca dispersión en su rango de datos y, por lo tanto, los cubosHashSet
está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
Point
estructura (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
GetHashCode
realiza:unchecked(x ^ y)
mientras que parastring
parece 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?list
cuando hayas terminado de poblarlo?point
,HashSet
se llamará internamenteGetHashCode
y para cada uno de esos puntos con el mismo código hash, se llamaráEquals
para determinar si ya existePoint
cuando puede crear una clase que implementeIEqualityComparer<Point>
y mantenga la compatibilidad con otras cosas con las que trabajaPoint
mientras obtiene el beneficio de no tener a los pobresGetHashCode
y la necesidad de encajonarEquals()
.