为什么HashSet <Point>比HashSet <string>慢得多?

我想存储一些像素位置而不允许重复,所以首先想到的是HashSet<Point>或类似的类。 然而,与HashSet<string>类似,这似乎很慢。

例如,这个代码:

 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)); } } } 

大约需要22.5秒。

虽然下面的代码(这显然不是一个好的select)只需要1.6秒:

 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); } } } 

所以,我的问题是:

  • 这是有原因吗? 我检查了这个答案 ,但22.5秒远远超过了答案中显示的数字。
  • 有没有更好的方法来存储点没有重复?

有两个由Point结构引起的性能问题。 当你添加Console.WriteLine(GC.CollectionCount(0)); 到testing代码。 你会看到Pointtesting需要〜3720个集合,但是stringtesting只需要~18个集合。 不是免费的。 当你看到一个价值types诱发这么多的collections,那么你需要得出结论“呃,太多的拳击”。

问题是HashSet<T>需要一个IEqualityComparer<T>来完成它的工作。 由于您没有提供,所以需要回退到EqualityComparer.Default<T>()返回的EqualityComparer.Default<T>() 。 该方法可以为string做好工作,它实现了IEquatable。 但是对于Point来说,从.NET 1.0起就是一种types,从来没有得到generics的爱。 它所能做的只是使用Object方法。

另一个问题是,Point.GetHashCode()在这个testing中没有做太多的工作,太多的碰撞,所以它很大程度上影响了Object.Equals()。 string有一个很好的GetHashCode实现。

通过为HashSet提供一个好的比较器,可以解决这两个问题。 像这个:

 class PointComparer : IEqualityComparer<Point> { public bool Equals(Point x, Point y) { return xX == yX && xY == yY; } public int GetHashCode(Point obj) { // Perfect hash for practical bitmaps, their width/height is never >= 65536 return (obj.Y << 16) ^ obj.X; } } 

并使用它:

 HashSet<Point> list = new HashSet<Point>(new PointComparer()); 

现在速度快了150倍,轻松击败弦乐testing。

performance下降的主要原因是所有的拳击正在进行( 汉斯帕斯坦的答案已经解释过)。

除此之外,散列码algorithm使问题更加恶化,因为它会导致更多的Equals(object obj)调用,从而增加了装箱转换的数量。

另外请注意, Point的哈希码由x ^ y计算。 这在你的数据范围内产生很less的散布,因此HashSet的桶被过度填充 – 这种情况在string中不会发生,散列的散布要大得多。

你可以通过实现你自己的Point结构(微不足道的),并使用更好的散列algorithm来解决这个问题,例如通过移动坐标:

 (x << 16) ^ y 

有关散列码的一些很好的build议,请阅读Eric Lippert关于这个主题的博客文章 。