GetHashCode和Buckets

时间:2012-12-12 10:33:27

标签: c# arrays collections hashcode 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));
    }

我天真的想法是:让我们减少桶的数量(使用简单的模数),这应该可以提高性能。但它很糟糕(在我的系统上,需要大约4秒,50000次迭代)。我还想过,如果我只是将Id作为哈希码返回,性能应该很差,因为我最终会得到50000个桶。但情况正好相反,我想我只是简单地制作所谓的碰撞音而不是改进任何东西。但话又说回来,桶清单如何运作?

3 个答案:

答案 0 :(得分:3)

基本上Contains检查:

  1. 获取项目的哈希码。
  2. 查找相应的存储桶 - 这是基于项目的哈希码的直接数组查找。
  3. 如果存在存储桶,则尝试在存储桶中查找该项目 - 这会迭代存储桶中的所有项目。
  4. 通过限制存储桶的数量,您增加了每个存储桶中的项目数,从而增加了散列集必须迭代的项目数,检查是否相等,以便查看项是否存在。因此,查看给定项目是否存在需要更长的时间。

    你可能减少了hashset的内存占用量;你可能甚至减少了插入时间,尽管我对此表示怀疑。你没有减少存在检查时间。

答案 1 :(得分:1)

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

提供哈希表性能的是从密钥到哈希码的转换,这意味着它可以快速消除集合中的大多数项目。它必须考虑的唯一项目是同一个桶中的那些。如果你的桶很少,这意味着它可以消除更少的物品。

GetHashCode最糟糕的实施方式会导致所有项目进入同一个存储桶:

public override int GetHashCode() {
  return 0;
}

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

答案 2 :(得分:1)

一个简单的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>的工作是将大空间减少到适当数量的桶,这大约是集合中的项目数(通常在两倍之内)。