为什么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关于这个主题的博客文章 。