【问题标题】:Why is HashSet<Point> so much slower than HashSet<string>?为什么 HashSet<Point> 比 HashSet<string> 慢这么多?
【发布时间】:2018-02-18 22:19:11
【问题描述】:

我想在不允许重复的情况下存储一些像素位置,所以首先想到的是HashSet&lt;Point&gt; 或类似的类。但是,与 HashSet&lt;string&gt; 之类的内容相比,这似乎非常慢。

例如这段代码:

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

所以,我的问题是:

  • 有什么原因吗?我检查了this answer,但 22.5 秒比该答案中显示的数字多得多。
  • 有没有更好的方法来存储不重复的点?

【问题讨论】:

标签: c# .net performance collections hashset


【解决方案1】:

Point 结构会引发两个性能问题。将Console.WriteLine(GC.CollectionCount(0)); 添加到测试代码时可以看到的内容。您会看到 Point 测试需要 ~3720 个集合,但字符串测试只需要 ~18 个集合。不是免费的。当你看到一个值类型引发了这么多集合时,你需要得出结论“呃,哦,太多的装箱”。

问题在于HashSet&lt;T&gt; 需要IEqualityComparer&lt;T&gt; 才能完成其工作。由于您没有提供一个,它需要退回到EqualityComparer.Default&lt;T&gt;() 返回的一个。该方法可以很好地处理字符串,它实现了 IEquatable。但不适用于 Point,它是一种来自 .NET 1.0 的类型,并且从未得到泛型的喜爱。它所能做的就是使用 Object 方法。

另一个问题是 Point.GetHashCode() 在这个测试中表现不佳,太多的碰撞,所以它对 Object.Equals() 的打击很大。 String 具有出色的 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 倍,轻松击败了字符串测试。

【讨论】:

  • +1 用于提供 GetHashCode 方法实现。只是出于好奇,您是如何实现特定的 obj.X &lt;&lt; 16 | obj.Y; 的。
  • 它的灵感来自于鼠标在窗口中通过其位置的方式。它是您想要显示的任何位图的完美哈希。
  • 很高兴知道这一点。有任何文档或最佳指南来编写像您这样的哈希码吗?实际上,我仍然想知道上面的哈希码是否带有您的经验或您遵循的任何指南。
  • @AkashKC 我对 C# 不是很熟悉,但据我所知,整数通常是 32 位。在这种情况下,您需要 2 个数字的散列,并通过将一个 16 位左移,确保每个数字的“低”16 位不会“影响”另一个与 |。对于 3 个数字,使用 22 和 11 作为班次是有意义的。对于 4 个数字,它将是 24、16、8。但是仍然会有冲突,但前提是数字变大。但它也关键取决于HashSet 的实现。如果它使用带有“位截断”的开放寻址(我不认为它这样做!)左移方法可能不好。
  • @HansPassant:我想知道在 GetHashCode 中使用 XOR 而不是 OR 是否会稍微好一些 - 如果点坐标可能超过 16 位(可能不在普通显示器上,但在不久的将来)。 // XOR 在散列函数中通常比 OR 更好,因为它丢失的信息更少,是可逆的,等等。如果允许负坐标,请考虑如果 Y 为负,X 贡献会发生什么。
【解决方案2】:

性能下降的主要原因是正在进行的所有拳击(正如Hans Passant's 答案中已经解释的那样)。

除此之外,哈希码算法使问题更加严重,因为它会导致更多对Equals(object obj) 的调用,从而增加装箱转换的数量。

还要注意the hash code of Point 是由x ^ y 计算的。这会在您的数据范围内产生非常小的分散,因此HashSet 的存储桶会被过度填充——string 不会发生这种情况,其中散列的分散要大得多。

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

(x << 16) ^ y

有关哈希码的一些好的建议,请阅读Eric Lippert's blog post on the subject

【讨论】:

  • @MartinSmith 因为没有其他充分的理由,如果哈希集对于一种类型而不是在相同情况下的另一种类型很慢,那么它必须是由于慢速类型的哈希码实现不是产生足够的分散。
  • 查看GetHashCode 执行的Point 的参考源:unchecked(x ^ y) 而对于string,它看起来要复杂得多..
  • @AhmedAbdelhameed 这可能是因为您向哈希集中添加的成员比您意识到的要少得多(同样是由于哈希码算法的可怕分散)。完成填充后,list 的计数是多少?
  • @AhmedAbdelhameed 你的测试是错误的。您一遍又一遍地添加相同的 long,因此实际上您插入的元素很少。插入point 时,HashSet 将在内部调用GetHashCode,并且对于具有相同哈希码的每个点,将调用Equals 以确定它是否已经存在
  • 当您可以创建一个实现IEqualityComparer&lt;Point&gt; 的类并与其他与Point 一起工作的东西保持兼容性时,无需实现Point,同时获得没有可怜的@987654342 的好处@ 并且需要在Equals() 中输入框。
猜你喜欢
  • 2013-04-23
  • 2018-10-31
  • 1970-01-01
  • 1970-01-01
  • 2021-12-25
  • 2014-12-24
  • 1970-01-01
  • 1970-01-01
  • 2013-09-30
相关资源
最近更新 更多