我试图更好地了解散列集的内部,例如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个桶。但情况正好相反,我想我只是简单地制作所谓的碰撞音而不是改进任何东西。但话又说回来,桶清单如何运作?
答案 0 :(得分:3)
基本上Contains
检查:
通过限制存储桶的数量,您增加了每个存储桶中的项目数,从而增加了散列集必须迭代的项目数,检查是否相等,以便查看项是否存在。因此,查看给定项目是否存在需要更长的时间。
你可能减少了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>
的工作是将大空间减少到适当数量的桶,这大约是集合中的项目数(通常在两倍之内)。