繁体   English   中英

GetHashCode和存储桶

[英]GetHashCode and Buckets

我试图更好地了解散列集(例如HashSet<T>如何工作以及它们为何表现出色。 我发现了以下文章,使用存储桶列表http://ericlippert.com/2011/02/28/guidelines-and-rules-for-gethashcode/实现了一个简单示例。

据我对本文的理解(我之前也曾这么认为),存储桶列表本身将每个存储桶中的一定数量的元素分组。 一个存储桶由哈希码表示,即由在元素上调用的GetHashCode表示。 我认为更好的性能是基于以下事实:存储桶少于元素。

现在,我编写了以下朴素的测试代码:

    public class CustomHashCode
    {
        public int Id { get; set; }

        public override int GetHashCode()
        {
            //return Id.GetHashCode(); // Way better performance
            return Id % 40; // Bad performance! But why?
        }


        public override bool Equals(object obj)
        {
            return ((CustomHashCode) obj).Id == Id;
        }

    }

这是探查器:

    public static void TestNoCustomHashCode(int iterations)
    {

        var hashSet = new HashSet<NoCustomHashCode>();
        for (int j = 0; j < iterations; j++)
        {
            hashSet.Add(new NoCustomHashCode() { Id = j });
        }

        var chc = hashSet.First();
        var stopwatch = new Stopwatch();
        stopwatch.Start();
        for (int j = 0; j < iterations; j++)
        {
            hashSet.Contains(chc);
        }
        stopwatch.Stop();

        Console.WriteLine(string.Format("Elapsed time (ms): {0}", stopwatch.ElapsedMilliseconds));
    }

我的天真想法是:让我们减少存储桶的数量(使用简单的模),这可以提高性能。 但这是可怕的(在我的系统上,迭代5万次大约需要4秒钟)。 我还认为,如果我只是将Id作为哈希码返回,则性能会很差,因为最终会得到50000个存储桶。 但是情况恰恰相反,我想我只是产生了所谓的碰撞声,而不是改善任何东西。 但是话又说回来,存储桶列表如何工作?

A Contains检查基本上:

  1. 获取项目的哈希码。
  2. 查找相应的存储桶-这是基于项目的哈希码的直接数组查找。
  3. 如果存储桶存在,请尝试在存储桶中查找项目-遍历存储桶中的所有项目。

通过限制存储桶的数量,您增加了每个存储桶中的项目数量,从而增加了哈希集必须迭代通过的项目数量,以检查是否相等,以查看某个项目是否存在。 因此,需要更长的时间才能查看给定的项目是否存在。

您可能已经减少了哈希集的内存占用; 你甚至可能减少插入时间,但我对此表示怀疑。 您尚未减少存在检查时间。

减少存储桶数量不会提高性能。 实际上, Int32GetHashCode方法本身返回整数值,这对于性能而言是理想的,因为它将产生尽可能多的存储桶。

赋予哈希表性能的是密钥到哈希码的转换,这意味着它可以快速消除集合中的大多数项目。 它必须考虑的唯一项目是同一存储桶中的项目。 如果您的水桶很少,则意味着它可以淘汰少得多的物品。

最糟糕的GetHashCode实现将导致所有项目进入同一存储桶:

public override int GetHashCode() {
  return 0;
}

这仍然是有效的实现,但是这意味着哈希表具有与常规列表相同的性能,即,它必须遍历集合中的所有项目以找到匹配项。

一个简单的HashSet<T>可以这样实现(只是一个草图,不会编译)

class HashSet<T>
{
    struct Element
    {
        int Hash;
        int Next;
        T item;
    }

    int[] buckets=new int[Capacity];
    Element[] data=new Element[Capacity];

    bool Contains(T item)
    {
        int hash=item.GetHashCode();
        // Bucket lookup is a simple array lookup => cheap
        int index=buckets[(uint)hash%Capacity];
        // Search for the actual item is linear in the number of items in the bucket
        while(index>=0)
        {
           if((data[index].Hash==hash) && Equals(data[index].Item, item))
             return true;
           index=data[index].Next;          
        }
        return false;
    }
}

如果您查看此内容,则在Contains中搜索的成本与存储桶中的项目数成正比。 因此,拥有更多的存储桶会使搜索更加便宜,但是一旦存储桶数量超过了商品数量,其他存储桶的收益就会迅速减少。

具有不同的哈希码还可以作为比较存储桶中对象的早期方法,从而避免了潜在的昂贵Equals调用。

简而言之, GetHashCode应该尽可能多样化。 HashSet<T>的工作是将大空间减少到适当数量的存储桶,这大约是集合中项目的数量(通常在两倍之内)。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM