C# 为什么HashSet<;点>;比HashSet慢得多<;字符串>;?

C# 为什么HashSet<;点>;比HashSet慢得多<;字符串>;?,c#,.net,performance,collections,hashset,C#,.net,Performance,Collections,Hashset,我想在不允许重复的情况下存储一些像素位置,所以首先想到的是HashSet或类似的类。然而,与HashSet之类的东西相比,这似乎非常慢 例如,此代码: HashSet<Point> points = new HashSet<Point>(); using (Bitmap img = new Bitmap(1000, 1000)) { for (int x = 0; x < img.Width; x++) { for (int y =

我想在不允许重复的情况下存储一些像素位置,所以首先想到的是
HashSet
或类似的类。然而,与HashSet之类的东西相比,这似乎非常慢

例如,此代码:

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));
        }
    }
}
HashSet points=newhashset();
使用(位图img=新位图(10001000))
{
对于(int x=0;x
大约需要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);
        }
    }
}
HashSet points=newhashset();
使用(位图img=新位图(10001000))
{
对于(int x=0;x
所以,我的问题是:

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

性能下降的主要原因是所有拳击比赛都在进行(答案中已经解释过)

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

还请注意,这是通过
x^y
计算的。这在您的数据范围内产生的离散度很小,因此
散列集
的存储桶人口过多,而
字符串
中散列的离散度要大得多

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

(x << 16) ^ y

(x点结构导致了两个性能问题。添加
Console.WriteLine(GC.CollectionCount(0))时可以看到这两个问题;
到测试代码。你会看到点测试需要~3720个集合,而字符串测试只需要~18个集合。这不是免费的。当你看到一个值类型诱导了这么多集合时,你需要总结“哦,太多的装箱”

问题在于
HashSet
需要一个
IEqualityComparer
来完成它的工作。因为您没有提供一个,所以它需要返回到
EqualityComparer.Default()返回的值
。该方法可以很好地处理字符串,它实现了IEquatable。但就这点而言,它是一种从.NET 1.0开始的类型,从未受到泛型的喜爱。它所能做的就是使用对象方法

另一个问题是,Point.GetHashCode()在这个测试中做得并不出色,因为冲突太多,所以它对Object.Equals()的影响很大。String有一个出色的GetHashCode实现

您可以通过为哈希集提供一个好的比较器来解决这两个问题。如下所示:

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;
    }
}
类指针比较器:IEqualityComparer{
公共布尔等于(点x,点y){
返回x.x==y.x&&x.y==y.y;
}
公共int GetHashCode(点obj){
//实用位图的完美散列,其宽度/高度从不>=65536

return(obj.Y@MartinSmith,因为没有其他好的理由,如果一个哈希集在相同的情况下对一种类型慢,而对另一种类型慢,那么它必须是由于慢类型的哈希代码实现没有产生足够的分散度。查看
GetHashCode
执行点的参考源:
未选中(x^y)
而对于
string
它看起来要复杂得多。@AhmedAbdelhameed这可能是因为您向哈希集中添加的成员比您想象的要少(同样是由于哈希代码算法的可怕分散)。当您完成填充后,
列表的计数是多少?@Ahmedabdelhamed您的测试是错误的。您反复添加相同的长度,因此实际上插入的元素很少。当插入
点时,
哈希集
将在内部调用
GetHashCode
,并针对每个点使用相同hashcode的ts将调用
Equals
来确定它是否已经存在。当您可以创建一个实现
IEqualityComparer
的类,并与使用
Point
的其他东西保持兼容时,就不需要实现
Point
,同时获得不使用穷人
GetH的好处ashCode
和需要在
中加框等于()
+1,用于提供GetHashCode方法实现。出于好奇,您是如何获得特定的
obj.X的?它的灵感来自于鼠标在windows中通过其位置的方式。它是您想要显示的任何位图的完美哈希。很高兴知道这一点。有任何文档或最佳指南来编写类似您的hashcode吗?Actually,我仍然想知道上面的哈希代码是你的经验还是你遵循的任何准则。@AkashKC我对C#不是很有经验,但据我所知,整数通常是32位。在这种情况下,你需要2个数字的哈希,通过左移1个16位,你可以确保每个数字的“低”16位不会“影响”另一个是带有
|
。对于3个数字,使用22和11作为移位是有意义的。对于4个数字,它将是24、16、8。但是,仍然会有冲突,但只有当数字变大时才会发生。但这也关键取决于
哈希集
的实现。如果它使用带“位截断”的开放寻址(我认为不会!)左移位方法可能不好。@HansPassant:我想知道在GetHashCode中使用XOR而不是OR是否会稍微好一点——如果点坐标可能超过16位(可能是n位)
HashSet<Point> list = new HashSet<Point>(new PointComparer());