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


165

我想存储一些像素位置而不允许重复,所以首先想到的是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秒。

尽管以下代码(出于明显的原因,这不是一个很好的选择)仅花费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秒比该答案中显示的数字还要多。
  • 有没有更好的方法来存储点而不重复?


不使用串联字符串的这些“明显原因”是什么?如果我不想实现自己的IEqualityComparer,哪种更好的方法呢?
伊万·尤琴科

Answers:


290

Point结构引发了两个性能问题。添加Console.WriteLine(GC.CollectionCount(0));到测试代码后,您会看到一些东西。您会看到Point测试需要〜3720个集合,而字符串测试只需要〜18个集合。不是免费的。当您看到一个值类型引发了如此多的集合时,您需要得出结论“呃,哦,太多拳击”。

问题在于HashSet<T>需要一项IEqualityComparer<T>工作来完成。由于您未提供,因此需要回退为EqualityComparer.Default<T>()。该方法可以很好地处理字符串,它实现了IEquatable。但是不是Point的一种,它是从.NET 1.0衍生出来的一种类型,从未得到泛型的热爱。它所能做的就是使用Object方法。

另一个问题是Point.GetHashCode()在此测试中没有完成出色的工作,发生了太多的碰撞,因此它严重地锤击了Object.Equals()。字符串具有出色的GetHashCode实现。

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

class PointComparer : IEqualityComparer<Point> {
    public bool Equals(Point x, Point y) {
        return x.X == y.X && x.Y == y.Y;
    }

    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倍,可以轻松击败字符串测试。


26
+1用于提供GetHashCode方法的实现。出于好奇,您是如何进行特定obj.X << 16 | obj.Y;实施的。
Akash KC

32
它的灵感来自鼠标在窗口中通过其位置的方式。对于您想显示的任何位图而言,这都是完美的哈希值。
汉斯·帕桑

2
很高兴知道。是否有任何文档或最佳指南来编写像您一样的哈希码?实际上,我仍然想知道上面的哈希码是否随您的经验或您遵循的任何准则一起提供。
Akash KC

5
@AkashKC我对C#不太了解,但据我所知,整数通常为32位。在这种情况下,您需要2个数字的哈希,并通过左移一个16位来确保每个数字的“低” 16位不会与另一个“影响” |。对于3个数字,可以使用22和11作为移位。对于4个数字,它将是24、16、8。但是,仍然存在冲突,但前提是数字变大。但这也关键取决于HashSet实施。如果它使用带有“位截断”的开放式地址(我不认为这样做!),则左移方法可能是不好的。
MSeifert

3
@HansPassant:我不知道在GetHashCode中使用XOR而不是OR会更好些吗-如果点坐标可能超过16位(也许不是在普通显示器上,而是在不久的将来)。// XOR在散列函数中通常比OR更好,因为它丢失的信息更少,是可逆的,等等。//例如,如果允许使用负坐标,请考虑如果Y为负,那么X贡献会发生什么。
Krazy Glew

85

性能下降的主要原因是所有拳击动作(如汉斯·帕桑特的回答中所述)。

除此之外,哈希码算法使问题变得更糟,因为它引起更多的调用,Equals(object obj)从而增加了装箱转换的数量。

另请注意,的哈希码由Point计算x ^ y。这在您的数据范围内产生的分散很小,因此,的存储桶中的HashSet人口过多- string散布的散布要大得多的情况不会发生。

您可以通过实现自己的Point结构(平凡的)并针对期望的数据范围使用更好的哈希算法来解决该问题,例如通过移动坐标:

(x << 16) ^ y

有关哈希码的一些好的建议,请阅读Eric Lippert关于该主题的博客文章


4
看点的参考来源GetHashCode表演:unchecked(x ^ y)虽然string它看起来要复杂得多
。–吉拉德·格林

2
嗯..为了检查您的假设是否正确,我只是尝试使用HashSet<long>()代替,然后list.Add(unchecked(x ^ y));将值添加到HashSet中。实际上,这甚至比HashSet<string> (345 ms)还要快。这与您描述的有所不同吗?
艾哈迈德·阿卜杜勒·哈默德

4
@AhmedAbdelhameed可能是因为您向哈希集添加的成员数量比您意识到的要少(再次由于哈希代码算法的分散性)。list完成填充后的计数是多少?
其间的

4
@AhmedAbdelhameed您的测试错误。一遍又一遍地添加相同的long,因此实际上您插入的元素很少。插入时pointHashSetwill会在内部调用,GetHashCode并为每个具有相同哈希码的点进行调用Equals以确定其是否已经存在
Ofir Winegarten

49
有没有必要实施Point的时候,你可以创建一个类实现IEqualityComparer<Point>与其他事物保持兼容性与工作Point同时获得不具有穷人的利益GetHashCode,并在需要框Equals()
乔恩·汉纳
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.