Eu queria armazenar algumas localizações de pixels sem permitir duplicatas, então a primeira coisa que vem à mente é HashSet<Point>
ou classes semelhantes. No entanto, isso parece ser muito lento em comparação com algo comoHashSet<string>
.
Por exemplo, 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));
}
}
}
leva cerca de 22,5 segundos.
Enquanto o código a seguir (que não é uma boa escolha por razões óbvias) leva apenas 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);
}
}
}
Então, minhas perguntas são:
- Existe uma razão para isso? Eu verifiquei esta resposta , mas 22,5 segundos é muito mais do que os números mostrados nessa resposta.
- Existe uma maneira melhor de armazenar pontos sem duplicatas?
c#
.net
performance
collections
hashset
Ahmed Abdelhameed
fonte
fonte
Respostas:
Existem dois problemas de desempenho induzidos pela estrutura Point. Algo que você pode ver quando adiciona
Console.WriteLine(GC.CollectionCount(0));
ao código de teste. Você verá que o teste de ponto requer ~ 3720 coleções, mas o teste de cadeia precisa apenas de ~ 18 coleções. Não de graça. Quando você vê um tipo de valor induzir tantas coleções, precisa concluir "uh-oh, muito boxe".O problema é que
HashSet<T>
precisa de umIEqualityComparer<T>
para fazer seu trabalho. Como você não forneceu um, ele precisa retornar ao retornadoEqualityComparer.Default<T>()
. Esse método pode fazer um bom trabalho para string, implementa IEquatable. Mas não para o Point, é um tipo que remete ao .NET 1.0 e nunca recebeu o amor dos genéricos. Tudo o que você pode fazer é usar os métodos Object.A outra questão é que Point.GetHashCode () não faz um trabalho estelar neste teste, muitas colisões, então martela muito o Object.Equals (). String possui uma excelente implementação GetHashCode.
Você pode resolver os dois problemas fornecendo ao HashSet um bom comparador. Como este:
E use-o:
E agora é 150 vezes mais rápido, superando facilmente o teste de cordas.
fonte
obj.X << 16 | obj.Y;
implementação específica .|
. Para 3 números, poderia fazer sentido usar 22 e 11 como turno. Para 4 números, seria 24, 16, 8. No entanto, ainda haverá colisões, mas apenas se os números aumentarem. Mas isso também depende crucialmente daHashSet
implementação. Se ele usa o endereço aberto com "truncamento de bits" (acho que não!), A abordagem à esquerda pode ser ruim.A principal razão para a queda no desempenho é todo o boxe (como já explicado na resposta de Hans Passant ).
Além disso, o algoritmo de código hash piora o problema, porque causa mais chamadas para
Equals(object obj)
aumentar a quantidade de conversões de boxe.Observe também que o código hash de
Point
é calculado porx ^ y
. Isso produz muito pouca dispersão no seu intervalo de dados e, portanto, os depósitosHashSet
são superpovoados - algo que não acontecestring
, onde a dispersão dos hashes é muito maior.Você pode resolver esse problema implementando sua própria
Point
estrutura (trivial) e usando um algoritmo de hash melhor para o intervalo de dados esperado, por exemplo, deslocando as coordenadas:Para alguns bons conselhos sobre códigos de hash, leia o post de Eric Lippert no blog sobre o assunto .
fonte
GetHashCode
executa:unchecked(x ^ y)
enquanto que parastring
ele parece muito mais complicado ..HashSet<long>()
vez disso e useilist.Add(unchecked(x ^ y));
para adicionar valores ao HashSet. Isso foi ainda mais rápido queHashSet<string>
(345 ms) . Isso é de alguma forma diferente do que você descreveu?list
quando você termina de preenchê-lo?point
, oHashSet
chamará internamenteGetHashCode
e para cada um desses pontos com o mesmo código hash, vai chamarEquals
para determinar se ele já existePoint
quando você pode criar uma classe que implementeIEqualityComparer<Point>
e mantenha a compatibilidade com outras coisas com as quais trabalha,Point
enquanto obtém o benefício de não ter os pobresGetHashCode
e a necessidade de se encaixarEquals()
.