什么是最佳的重写GetHashCode算法?

1672
在.NET中,GetHashCode 方法 在许多.NET基类库中被广泛使用。正确实现它对于快速查找集合中的项或确定相等性尤为重要。 是否有标准算法或最佳实践来为我的自定义类实现GetHashCode,以免降低性能?

48
阅读此问题和下面的文章后,我可以实现GetHashCode的重写。希望对他人有所帮助。由Eric Lippert编写的GetHashCode指南和规则 - rene
6
“或者确定相等性”:不!具有相同哈希码的两个对象未必相等。 - Thomas Levesque
4
你说得对,具有相同哈希码的两个对象不一定相等。但是在很多实现Equals()的情况下,仍然使用了GetHashCode()函数。这就是我之前所说的意思。将GetHashCode()嵌套在Equals()中经常被用作快捷方式来确定不相等性,因为如果两个对象具有不同的哈希码,则它们必须是不相等的对象,而其余的相等性检查就不必执行了。 - bitbonk
7
通常情况下,GetHashCode()Equals()两者都需要查看两个对象的所有字段(如果哈希码相等或未检查,则Equals()需要执行此操作)。因此,在Equals()内部调用GetHashCode()通常是多余的,并且可能会降低性能。Equals()也可以进行短路运算,使其更快 - 但在某些情况下,哈希码可能已被缓存,从而使GetHashCode()检查更快,因此值得使用。请参见此问题以了解更多信息。 - NotEnoughData
12
2020年1月更新:Eric Lippert的博客位于:https://learn.microsoft.com/en-us/archive/blogs/ericlippert/guidelines-and-rules-for-gethashcode - Rick Davin
显示剩余2条评论
22个回答

1803

通常我会使用Josh Bloch在他精彩的Effective Java书中提供的实现。它快速并且创建了一个相当不容易造成碰撞的哈希值。选择两个不同的质数,例如17和23,然后进行以下操作:

public override int GetHashCode()
{
    unchecked // Overflow is fine, just wrap
    {
        int hash = 17;
        // Suitable nullity checks etc, of course :)
        hash = hash * 23 + field1.GetHashCode();
        hash = hash * 23 + field2.GetHashCode();
        hash = hash * 23 + field3.GetHashCode();
        return hash;
    }
}

正如评论中所指出的那样,您可能会发现最好选择一个大的素数来进行乘法运算。显然,486187739是不错的选择...虽然我看到的大多数使用小数的示例通常使用质数,但至少有一些类似的算法经常使用非质数。例如,在稍微改变的FNV示例中,我使用了一些可以很好地工作的数字,但初始值不是质数。(不过,乘法常数是质数。我不知道这有多重要。)

这比常见做法——对哈希码进行XOR——要好,主要有两个原因。假设我们有一个带有两个int字段的类型:

XorHash(x, x) == XorHash(y, y) == 0 for all x, y
XorHash(x, y) == XorHash(y, x) for all x, y

顺便提一下,早期的算法是C#编译器目前用于匿名类型的算法。

这个页面提供了很多选择。我认为对于大多数情况来说,以上方法已经足够好,并且非常容易记住和实现。 FNV 替代方案同样简单,但使用不同的常量,使用 XOR 而不是 ADD 作为组合操作。它看起来像下面的代码,但正常的 FNV 算法是针对每个字节进行操作的,因此需要修改以执行每个字节而不是每个32位哈希值的迭代。FNV 还设计用于可变长度的数据,而我们在这里使用它的方式总是相同数量的字段值。对这个答案的评论表明,这里的代码在(测试的)示例情况下实际上并不像上面的加法方法那样有效。

// Note: Not quite FNV!
public override int GetHashCode()
{
    unchecked // Overflow is fine, just wrap
    {
        int hash = (int) 2166136261;
        // Suitable nullity checks etc, of course :)
        hash = (hash * 16777619) ^ field1.GetHashCode();
        hash = (hash * 16777619) ^ field2.GetHashCode();
        hash = (hash * 16777619) ^ field3.GetHashCode();
        return hash;
    }
}

需要注意的一件事是,理想情况下,您应该在将敏感于相等性(因此敏感于哈希码)的状态添加到依赖于哈希码的集合之后,防止其更改。

根据文档

对于不可变的引用类型,可以重写 GetHashCode 。通常,对于可变的引用类型,只有在以下情况下才应覆盖 GetHashCode :

  • 可以从不可变字段计算哈希码;或者
  • 可以确保可变对象的哈希码在包含它的依赖于其哈希码的集合中不会更改。

FNV 文章的链接已失效,但是在 Internet Archive 中有备份: Eternally Confuzzled - The Art of Hashing


9
您提到的书中描述的算法实际上更加详细,尤其是针对不同字段数据类型的操作进行了描述。例如:对于长整型字段,使用(int)(field ^ f >>> 32)而不是简单地调用GetHashCode方法。长整型的GetHashCode方法是按照这种方式实现的吗? - bitbonk
15
是的,Int64.GetHashCode正是这样做的。当然,在Java中需要装箱。这让我想起了 - 是时候添加一本书的链接了... - Jon Skeet
85
23不是一个好的选择,因为(从.NET 3.5 SP1开始)Dictionary<TKey,TValue>假定在某些质数下具有良好的分布。而23就是其中之一。因此,如果您有一个容量为23的字典,那么只有最后一个对GetHashCode方法的贡献会影响复合哈希码。所以我更愿意使用29而不是23。 - CodesInChaos
28
@CodeInChaos:只有最后一次贡献会影响到桶,所以在最坏情况下,它可能需要查看字典中的 全部23个 条目。它仍然会检查每个条目的实际哈希码,这是廉价的。如果你有一个如此小的字典,它不太可能产生太大影响。 - Jon Skeet
25
通常我将0作为null的有效哈希码,这并不等同于忽略该字段。 - Jon Skeet
显示剩余82条评论

548

ValueTuple - C# 7的更新

正如@cactuaroid在评论中提到的,可以使用值元组(ValueTuple)。这样可以节省一些击键,并且更重要的是纯粹在堆栈上执行(没有垃圾):

(PropA, PropB, PropC, PropD).GetHashCode();
(注:使用匿名类型的原始技术似乎会在堆上创建一个对象,即垃圾,因为匿名类型实现为类,尽管编译器可能会优化掉这个问题。对这些选项进行基准测试将是有趣的,但元组选项应该更优。)

匿名类型(原始答案)

微软已经提供了一个很好的通用HashCode生成器:只需将您的属性/字段值复制到匿名类型中并进行哈希处理:

new { PropA, PropB, PropC, PropD }.GetHashCode();
这将适用于任何数量的属性。它不使用装箱,只是使用框架中已实现的匿名类型算法。

90
是的,匿名的 GetHashCode 实现非常有效(顺便说一句,这与 Jon Skeet 的答案中的实现相同),但是这种解决方案的唯一问题在于每次调用 GetHashCode 都会生成一个新的实例。在访问大型散列集合时可能会有点开销。 - digEmAll
5
@digEmAll 说得好,我没有考虑创建新对象的开销。Jon Skeet的答案是最有效率的,并且不会使用装箱操作。(@Kumba 要解决VB中的unchecked问题,只需使用Int64(long),并在计算后截断即可。) - Rick Love
20
在匿名类型创建时,VB.NET必须使用关键字Key:New With {Key PropA}.GetHashCode(),否则对于具有相同“标识”属性的不同对象,GetHashCode将不会返回相同的哈希码。 - David Osborne
4
在这种情况下,我会考虑将IEnumerable保存为列表值,并将其存储在某个地方,而不是每次计算哈希码时枚举它。在GetHashCode内部每次计算ToList可能会在许多情况下影响性能。 - Rick Love
9
如果您喜欢这个,(PropA, PropB, PropC, PropD).GetHashCode() 现在可以在 C#7 上使用,而无需担心 GC 压力,以回应 @digEmAll 的关注。快速简单的哈希码组合 - cactuaroid
显示剩余11条评论

148

使用 System.HashCode

如果你使用的是 .NET Standard 2.1 或以上版本,可以使用 System.HashCode 结构体。在早期的框架中,可以通过 Microsoft.Bcl.HashCode 包获得。有两种使用方法:

HashCode.Combine

Combine方法可用于创建哈希码,最多可给出八个对象。

public override int GetHashCode() => HashCode.Combine(this.object1, this.object2);

HashCode.Add

Add 方法可以帮助你处理集合:

public override int GetHashCode()
{
    var hashCode = new HashCode();
    hashCode.Add(this.object1);
    foreach (var item in this.collection)
    {
        hashCode.Add(item);
    }
    return hashCode.ToHashCode();
}

HashCode简化

这是一个替代System.HashCode的方法,使用起来非常简单,同时速度也很快。您可以阅读完整的博客文章'HashCode简化'以获取更多细节和评论。

使用示例

public class SuperHero
{
    public int Age { get; set; }
    public string Name { get; set; }
    public List<string> Powers { get; set; }

    public override int GetHashCode() =>
        HashCode.Of(this.Name).And(this.Age).AndEach(this.Powers);
}

实施

public struct HashCode : IEquatable<HashCode>
{
    private const int EmptyCollectionPrimeNumber = 19;
    private readonly int value;

    private HashCode(int value) => this.value = value;

    public static implicit operator int(HashCode hashCode) => hashCode.value;

    public static bool operator ==(HashCode left, HashCode right) => left.Equals(right);

    public static bool operator !=(HashCode left, HashCode right) => !(left == right);

    public static HashCode Of<T>(T item) => new HashCode(GetHashCode(item));

    public static HashCode OfEach<T>(IEnumerable<T> items) =>
        items == null ? new HashCode(0) : new HashCode(GetHashCode(items, 0));

    public HashCode And<T>(T item) => 
        new HashCode(CombineHashCodes(this.value, GetHashCode(item)));

    public HashCode AndEach<T>(IEnumerable<T> items)
    {
        if (items == null)
        {
            return new HashCode(this.value);
        }

        return new HashCode(GetHashCode(items, this.value));
    }

    public bool Equals(HashCode other) => this.value.Equals(other.value);

    public override bool Equals(object obj)
    {
        if (obj is HashCode)
        {
            return this.Equals((HashCode)obj);
        }

        return false;
    }

    public override int GetHashCode() => this.value.GetHashCode();

    private static int CombineHashCodes(int h1, int h2)
    {
        unchecked
        {
            // Code copied from System.Tuple a good way to combine hashes.
            return ((h1 << 5) + h1) ^ h2;
        }
    }

    private static int GetHashCode<T>(T item) => item?.GetHashCode() ?? 0;

    private static int GetHashCode<T>(IEnumerable<T> items, int startHashCode)
    {
        var temp = startHashCode;

        var enumerator = items.GetEnumerator();
        if (enumerator.MoveNext())
        {
            temp = CombineHashCodes(temp, GetHashCode(enumerator.Current));

            while (enumerator.MoveNext())
            {
                temp = CombineHashCodes(temp, GetHashCode(enumerator.Current));
            }
        }
        else
        {
            temp = CombineHashCodes(temp, EmptyCollectionPrimeNumber);
        }

        return temp;
    }
}

什么是好的算法?

性能

计算哈希码的算法需要快速。简单的算法通常会更快。不分配额外的内存也将减少垃圾回收的需求,从而提高性能。

在 C# 哈希函数中,通常使用 unchecked 关键字来停止溢出检查以提高性能。

确定性

哈希算法需要是确定性的,即给定相同的输入,它必须始终产生相同的输出。

减少冲突

计算哈希码的算法需要将哈希冲突保持到最小。哈希冲突是指当两个不同对象上的两个调用 GetHashCode 产生相同的哈希码时发生的情况。请注意,允许发生冲突(有些人误解为不允许),但应该尽量减少。

许多哈希函数包含像 1723 这样的特殊质数,由于它们的数学特性,可以帮助减少哈希冲突,而不是使用非质数。

哈希均匀性

一种良好的哈希函数应该尽可能均匀地将预期输入映射到其输出范围,即它应基于其输入输出广泛分布的哈希值。它应具有哈希均匀性。 防止DoS 在.NET Core中,每次重新启动应用程序都会获得不同的哈希码。这是一种防止拒绝服务(DoS)攻击的安全功能。对于.NET Framework,您应该通过添加以下App.config文件来启用此功能:
<?xml version ="1.0"?>  
<configuration>  
   <runtime>  
      <UseRandomizedStringHashAlgorithm enabled="1" />  
   </runtime>  
</configuration>
由于这个特性,哈希码不应在创建它们的应用程序域之外使用,不应作为集合中的关键字段使用,也不应持久化存储。 在此处了解更多信息。 加密安全吗? 该算法不必是加密哈希函数。意味着它不必满足以下条件: - 生成一个消息以产生给定哈希值是不可行的。 - 找到两个具有相同哈希值的不同消息是不可行的。 - 对消息进行微小更改应更改哈希值,使得新哈希值与旧哈希值不相关(雪崩效应)。

4
非常好的答案。作为补充,您可以考虑将“速度”更改为“性能”,并添加无需分配内存的属性。内置的 HashCode 类型也满足此要求。 - Timo
这与@ricklove最近更新的ValueTuple.GetHashCode()答案相比如何? - Thiago Silva
3
HashCode.Combine是一个静态方法,不会分配任何内存,而ValueTuple会从堆栈开始分配内存。 - Muhammad Rehan Saeed
3
HashCode.Of(this.Name).And(this.Age).AndEach(this.Powers) - 这是很好的语法 :) - Amos Egel
“它们不应该被用作集合中的关键字段”,但这不是哈希码的全部意义吗?还有哈希表、哈希集合和字典的存在呢? - maraaaaaaaa
非常好,但目前有两个点没有提到:首先,集合比较器尊重顺序(因此[1,2,3]的哈希码与[3,2,1]不同),这可能是期望的,但并非总是如此,因此至少在文档中应该明确说明。其次,可枚举对象将被完全迭代并且元素将被实例化。如果源是无限枚举或元素是即时创建的,则可能会导致问题。同样,在文档中应该提到这一点。 - Oliver

111

这是我的哈希码助手。
它的优点在于使用了通用类型参数,因此不会导致装箱:

public static class HashHelper
{
    public static int GetHashCode<T1, T2>(T1 arg1, T2 arg2)
    {
         unchecked
         {
             return 31 * arg1.GetHashCode() + arg2.GetHashCode();
         }
    }

    public static int GetHashCode<T1, T2, T3>(T1 arg1, T2 arg2, T3 arg3)
    {
        unchecked
        {
            int hash = arg1.GetHashCode();
            hash = 31 * hash + arg2.GetHashCode();
            return 31 * hash + arg3.GetHashCode();
        }
    }

    public static int GetHashCode<T1, T2, T3, T4>(T1 arg1, T2 arg2, T3 arg3, 
        T4 arg4)
    {
        unchecked
        {
            int hash = arg1.GetHashCode();
            hash = 31 * hash + arg2.GetHashCode();
            hash = 31 * hash + arg3.GetHashCode();
            return 31 * hash + arg4.GetHashCode();
        }
    }

    public static int GetHashCode<T>(T[] list)
    {
        unchecked
        {
            int hash = 0;
            foreach (var item in list)
            {
                hash = 31 * hash + item.GetHashCode();
            }
            return hash;
        }
    }

    public static int GetHashCode<T>(IEnumerable<T> list)
    {
        unchecked
        {
            int hash = 0;
            foreach (var item in list)
            {
                hash = 31 * hash + item.GetHashCode();
            }
            return hash;
        }
    }

    /// <summary>
    /// Gets a hashcode for a collection for that the order of items 
    /// does not matter.
    /// So {1, 2, 3} and {3, 2, 1} will get same hash code.
    /// </summary>
    public static int GetHashCodeForOrderNoMatterCollection<T>(
        IEnumerable<T> list)
    {
        unchecked
        {
            int hash = 0;
            int count = 0;
            foreach (var item in list)
            {
                hash += item.GetHashCode();
                count++;
            }
            return 31 * hash + count.GetHashCode();
        }
    }

    /// <summary>
    /// Alternative way to get a hashcode is to use a fluent 
    /// interface like this:<br />
    /// return 0.CombineHashCode(field1).CombineHashCode(field2).
    ///     CombineHashCode(field3);
    /// </summary>
    public static int CombineHashCode<T>(this int hashCode, T arg)
    {
        unchecked
        {
            return 31 * hashCode + arg.GetHashCode();   
        }
    }
也有扩展方法提供流畅的接口,所以您可以像这样使用它:
public override int GetHashCode()
{
    return HashHelper.GetHashCode(Manufacturer, PartN, Quantity);
}
或者像这样:
public override int GetHashCode()
{
    return 0.CombineHashCode(Manufacturer)
        .CombineHashCode(PartN)
        .CombineHashCode(Quantity);
}

5
不需要单独使用 T[] ,因为它已经是 IEnumerable<T> - nawfal
5
你可以重构那些方法,将核心逻辑限制在一个函数中。 - nawfal
15
顺便提一下,31 是 CPU 上的移位和减法操作,速度非常快。 - Chui Tey
5
你可以使用 params - ANeves
7
这是所有梅森质数共有的特征。 - Pharap
显示剩余4条评论

67

我有一个帮助库中的哈希类,我用它来实现这个目的。

/// <summary> 
/// This is a simple hashing function from Robert Sedgwicks Hashing in C book.
/// Also, some simple optimizations to the algorithm in order to speed up
/// its hashing process have been added. from: www.partow.net
/// </summary>
/// <param name="input">array of objects, parameters combination that you need
/// to get a unique hash code for them</param>
/// <returns>Hash code</returns>
public static int RSHash(params object[] input)
{
    const int b = 378551;
    int a = 63689;
    int hash = 0;

    // If it overflows then just wrap around
    unchecked
    {
        for (int i = 0; i < input.Length; i++)
        {
            if (input[i] != null)
            {
                hash = hash * a + input[i].GetHashCode();
                a = a * b;
            }
        }
    }

    return hash;
}
然后,你可以简单地将它用作:
public override int GetHashCode()
{
    return Hashing.RSHash(_field1, _field2, _field3);
}

我没有评估它的表现,所以欢迎任何反馈。


29
如果字段是值类型,那么这将导致拳击。 - nightcoder
7
“可以通过捕获OverflowException来增强后续处理。” unchecked的整个意义在于避免在GetHashCode上溢出时发生异常。因此,如果值超过了int,也不会有任何影响,它并不是错误的。 - Tim Schmelter
2
该算法的一个问题是,无论其长度如何,任何由null填充的数组始终会返回0。 - Nathan Adams
3
此辅助方法还会分配一个新的 object[] 对象。 - James Newton-King
2
正如@NathanAdams所提到的,跳过null可能会给您带来意想不到的结果。您应该在input[i]为null时使用一些常量值,而不是跳过它们,而不是使用input[i].GetHashCode() - David Schwartz

59
这是我的助手类,使用Jon Skeet的实现
public static class HashCode
{
    public const int Start = 17;

    public static int Hash<T>(this int hash, T obj)
    {
        var h = EqualityComparer<T>.Default.GetHashCode(obj);
        return unchecked((hash * 31) + h);
    }
}

使用方法:

public override int GetHashCode()
{
    return HashCode.Start
        .Hash(_field1)
        .Hash(_field2)
        .Hash(_field3);
}

如果您想避免编写 System.Int32 的扩展方法:

public readonly struct HashCode
{
    private readonly int _value;

    public HashCode(int value) => _value = value;

    public static HashCode Start { get; } = new HashCode(17);

    public static implicit operator int(HashCode hash) => hash._value;

    public HashCode Hash<T>(T obj)
    {
        var h = EqualityComparer<T>.Default.GetHashCode(obj);
        return unchecked(new HashCode((_value * 31) + h));
    }

    public override int GetHashCode() => _value;
}
它仍然避免了任何堆分配,并且使用方式完全相同:
public override int GetHashCode()
{
    // This time `HashCode.Start` is not an `Int32`, it's a `HashCode` instance.
    // And the result is implicitly converted to `Int32`.
    return HashCode.Start
        .Hash(_field1)
        .Hash(_field2)     
        .Hash(_field3);
}

编辑(2018年5月):EqualityComparer<T>.Default属性现在是JIT内置的。这个拉取请求被Stephen Toub提到,在这篇博客文章中提到了它。


1
我会将三元运算符的那一行改为:var h = Equals(obj, default(T)) ? 0 : obj.GetHashCode(); - Bill Barry
我相信带有 obj != null 的三元运算符将编译为一个 box 指令,如果 T 是值类型,则会分配内存。相反,您可以使用 obj.Equals(null),它将编译为 Equals 方法的虚拟调用。 - Martin Liversage
因为 this.hashCode != h,所以它不会返回相同的值。 - Şafak Gür
抱歉,我误删了我的评论而不是编辑它。创建一个新的结构体然后将hashCode更改为非只读,再执行以下操作是否更有益:“unchecked { this.hashCode ^= h * 397; } return this;”? - Erik Karlsson
不可变性有其好处(为什么可变结构体是邪恶的?)。关于性能,我所做的事情非常便宜,因为它不会在堆中分配任何空间。 - Şafak Gür
如果你像Hash(1)这样调用它而不是像Hash<MyInterface>(myStruct)这样调用它,就没有装箱。 - user764754

31
在大多数情况下,如果Equals()比较了多个字段,则在GetHash()上哈希一个字段或者多个字段并不重要。您只需确保计算哈希值非常便宜(请勿分配内存),速度快(没有重复的计算,当然也不存在数据库连接),并提供良好的分布。 处理繁重工作的应该是Equals()方法;哈希应该是一项非常廉价的操作,以尽可能少地调用Equals()。 最后一个提示:不要指望GetHashCode()在多个应用程序运行中稳定。许多.NET类型不能保证它们的哈希码在重新启动后保持不变,因此您只能将GetHashCode()的值用于内存数据结构中。

12
在大多数情况下,如果Equals()函数比较了多个字段,那么GetHash()函数散列一个字段或多个字段并不重要。但这是有风险的建议,因为对于只在未散列字段上有差异的对象,你会得到散列冲突。如果这种情况经常发生,基于哈希的集合(如HashMap、HashSet等)的性能将会下降(最坏情况下达到O(n))。 - sleske
12
这个故事发生在Java中:在JDK的早期版本中,String.hashCode()只考虑字符串的开头;如果你把字符串用作HashMap的键,但这些字符串只有结尾不同(例如URL),就会导致性能问题。因此,算法已经改变(我相信是在JDK 1.2或1.3中)。 - sleske
4
如果那个领域“提供了良好的分布”(我的回答的最后一部分),那么一个领域就足够了。但如果它“没有提供良好的分布”,那么你就需要进行另一个计算。(例如,只需使用另一个提供良好分布的领域,或使用多个领域)。 - Bert Huijben
我认为在GetHashCode执行内存分配方面没有问题,只要它仅在第一次使用时执行(随后的调用只返回缓存结果)。重要的是不应该费尽心思避免冲突,而是应该避免“系统性”冲突。如果一个类型有两个int字段oldXnewX,它们经常相差一,那么oldX^newX的哈希值将会给这样的记录分配90%的哈希值1、2、4或8。使用oldX+newX [未检查的算术]可能会生成更多的冲突... - supercat
1
比起更复杂的函数,一个包含100万个物品且有500,000个不同哈希值的集合,如果每个哈希值都有两个相关联的物品,那么它会表现得非常好;但如果一个哈希值有500,001个物品,而其他哈希值只有一个物品,则表现会非常糟糕。 - supercat

27
直到最近我的答案与Jon Skeet的答案非常相似。然而,我最近开始了一个使用二次幂哈希表的项目,即哈希表内部表的大小为8、16、32等。倾向于使用质数大小有充分的理由,但是使用二次幂大小也有一些优点。 而且这几乎是失败的。所以在一番试验和研究之后,我开始使用以下方式重新哈希我的哈希表:
public static int ReHash(int source)
{
  unchecked
  {
    ulong c = 0xDEADBEEFDEADBEEF + (ulong)source;
    ulong d = 0xE2ADBEEFDEADBEEF ^ c;
    ulong a = d += c = c << 15 | c >> -15;
    ulong b = a += d = d << 52 | d >> -52;
    c ^= b += a = a << 26 | a >> -26;
    d ^= c += b = b << 51 | b >> -51;
    a ^= d += c = c << 28 | c >> -28;
    b ^= a += d = d << 9 | d >> -9;
    c ^= b += a = a << 47 | a >> -47;
    d ^= c += b << 54 | b >> -54;
    a ^= d += c << 32 | c >> 32;
    a += d << 25 | d >> -25;
    return (int)(a >> 1);
  }
}
然后我的二次幂哈希表不再糟糕了。但这让我感到困扰,因为上述方法本不应奏效。更确切地说,除非原始的 GetHashCode() 以特定的方式较差,否则它不应奏效。 重新混合哈希码无法改善很好的哈希码,因为唯一可能的影响是引入更多冲突。 重新混合哈希码无法改善可怕的哈希码,因为唯一可能的影响是将值为53的大量冲突更改为183487291的大量冲突。 重新混合哈希码只能改善避免绝对冲突的哈希码,但在实际使用哈希表时却很差。虽然使用二次幂表格的简单取模使这更加明显,但在使用更常见的质数表格时也会产生负面影响,只是不太明显(重新哈希的额外工作会抵消收益,但仍然会有收益)。 编辑:我还使用了开放地址,这也增加了对冲突的敏感性,可能比二次幂更甚。

好的,关于编程方面的内容,我来为您翻译一下。在.NET(或者可以在这里了解)中,string.GetHashCode()实现的质量有待提高,通过改进可以减少冲突,测试运行速度快了20-30倍,这让人感到不安,更令人不安的是我的哈希代码也可以得到大幅改善。

过去我编写的所有GetHashCode()实现方法,包括我在本站回答问题时使用的方法,都比我想象中要差得多。虽然很多时候这已经足够应对大部分情况,但我想要更好的方法。

所以我把那个项目放在一边(它只是一个个人项目),开始寻找如何快速在.NET中生成一个好的、分布均匀的哈希代码。

最终我选择将SpookyHash移植到.NET。事实上,上面的代码是使用SpookyHash从32位输入产生32位输出的快速路径版本。

现在,SpookyHash并不是一个容易记忆的好代码。我手动内联了很多内容以提高速度*,所以我对它进行了移植,使它变得更加不容易理解。但这就是代码重用的作用。 然后我把那个项目放到一边,因为原始项目产生了一个问题:如何生成更好的哈希码,而那个项目则产生了一个问题:如何生成更好的.NET memcpy。 然后我回来了,并产生了很多重载,以便轻松地将几乎所有本机类型(除了decimal†)输入哈希码。 它很快,其中Bob Jenkins应该得到大部分的功劳,因为他的原始代码比我移植的代码更快,特别是在64位机器上,该算法经过了优化‡。 完整的代码可以在https://bitbucket.org/JonHanna/spookilysharp/src中查看,但请考虑上面的代码是其简化版本。 然而,既然它已经被写出来了,那么人们可以更容易地利用它:
public override int GetHashCode()
{
  var hash = new SpookyHash();
  hash.Update(field1);
  hash.Update(field2);
  hash.Update(field3);
  return hash.Final().GetHashCode();
}
它还可以接受种子值,因此如果您需要处理不受信任的输入并希望防止哈希DoS攻击,可以基于运行时间等设置种子,并使攻击者无法预测结果。
private static long hashSeed0 = Environment.TickCount;
private static long hashSeed1 = DateTime.Now.Ticks;
public override int GetHashCode()
{
  //produce different hashes ever time this application is restarted
  //but remain consistent in each run, so attackers have a harder time
  //DoSing the hash tables.
  var hash = new SpookyHash(hashSeed0, hashSeed1);
  hash.Update(field1);
  hash.Update(field2);
  hash.Update(field3);
  return hash.Final().GetHashCode();
}

*在这里的一个大惊喜是,手动内联旋转方法并返回(x << n) | (x >> -n)可以改善性能。我本以为即时编译器会自动内联它,但分析结果表明不是这样。

†从.NET的角度来看,decimal并不是本地的,尽管在C#中是。它的问题在于其自己的GetHashCode()将精度视为重要因素,而其自己的Equals()则不是。这两种方式都是有效的选择,但不能混合使用。在实现自己的版本时,您需要选择其中一种,但我无法知道您想要哪个。

‡作为比较。如果用于字符串,64位的SpookyHash比32位的string.GetHashCode()要快得多,而32位的string.GetHashCode()略快于64位的string.GetHashCode(),后者又比32位的SpookyHash快得多,尽管仍然足够快以成为合理的选择。


当将多个哈希值合并为一个时,我倾向于使用long类型的中间结果,然后将最终结果压缩为int类型。这样做是否明智?我的担忧是,如果使用例如hash=(hash*31)+nextField,那么匹配值对只会影响哈希的上27位。让计算扩展到long类型并进行包装可以最小化这种危险。 - supercat
@JonHanna,你能否更精确地描述你遇到的问题行为?我正在尝试实现一个使值对象实现变得微不足道的库(ValueUtils),我希望有一个测试集来展示在二次幂哈希表中劣质杂糅的情况。 - Eamon Nerbonne
@EamonNerbonne 我没有比“那种方式的总时间更慢”的更精确的东西。正如我在编辑中添加的那样,我使用开放地址可能比二次幂因子更重要。我计划在一个特定项目上进行一些测试用例,比较几种不同的方法,所以在那之后我可能会有更好的答案,尽管这不是高优先级(一个个人项目没有紧迫的需求,所以我会在我想到它的时候去做...) - Jon Hanna
@JonHanna:是的,我知道个人项目进度如何 - 祝你好运!无论如何,我看到我上次评论的措辞不太好:我的意思是要求有问题的输入,而不一定是导致问题的细节。我很想将其用作测试集(或测试集灵感)。无论如何 - 祝你的宠物项目好运:-)。 - Eamon Nerbonne
我敢打赌你的ReHash有点过度了。我猜它能够正常工作,但它可能比加密哈希还要慢,而加密哈希已经被证明可以完美地工作。Java也使用大小为2的幂的表格,曾经有一个相当复杂的重新哈希过程。自从引入了树节点来处理冲突后,这个过程已经变得简化了。 - maaartinus
显示剩余2条评论

25

截至https://github.com/dotnet/coreclr/pull/14863,现在有一种全新的生成哈希码的方法,非常简单!只需编写:

public override int GetHashCode()
    => HashCode.Combine(field1, field2, field3);

这将生成一个优质的哈希码,而无需您担心实现细节。


看起来这是一个不错的补充...有没有办法知道它将会发布哪个版本的.NET Core? - Dan J
1
@DanJ 真是个巧合,HashCode 的更改在你的评论几个小时前已经合并到了 corefx 中 :) 这种类型预计将在 .NET Core 2.1 中发布。 - James Ko
真棒!而且转化时间非常快。已点赞。 :) - Dan J
@DanJ 更好的消息是——它现在应该已经可以在托管在dotnet-core MyGet feed上的CoreFX夜间构建中使用了。 - James Ko
甜的 - 这对我的工作没有帮助,因为我们还没有那么前沿,但是好知道。干杯! - Dan J
这里有一个可用于.NET 4.0+(包括System.HashCode)的即插即用polyfill包:https://www.nuget.org/packages/Gapotchenko.FX - ogggre

13

这是一个好的例子:

/// <summary>
/// Helper class for generating hash codes suitable 
/// for use in hashing algorithms and data structures like a hash table. 
/// </summary>
public static class HashCodeHelper
{
    private static int GetHashCodeInternal(int key1, int key2)
    {
        unchecked
        {
           var num = 0x7e53a269;
           num = (-1521134295 * num) + key1;
           num += (num << 10);
           num ^= (num >> 6);

           num = ((-1521134295 * num) + key2);
           num += (num << 10);
           num ^= (num >> 6);

           return num;
        }
    }

    /// <summary>
    /// Returns a hash code for the specified objects
    /// </summary>
    /// <param name="arr">An array of objects used for generating the 
    /// hash code.</param>
    /// <returns>
    /// A hash code, suitable for use in hashing algorithms and data 
    /// structures like a hash table. 
    /// </returns>
    public static int GetHashCode(params object[] arr)
    {
        int hash = 0;
        foreach (var item in arr)
            hash = GetHashCodeInternal(hash, item.GetHashCode());
        return hash;
    }

    /// <summary>
    /// Returns a hash code for the specified objects
    /// </summary>
    /// <param name="obj1">The first object.</param>
    /// <param name="obj2">The second object.</param>
    /// <param name="obj3">The third object.</param>
    /// <param name="obj4">The fourth object.</param>
    /// <returns>
    /// A hash code, suitable for use in hashing algorithms and
    /// data structures like a hash table.
    /// </returns>
    public static int GetHashCode<T1, T2, T3, T4>(T1 obj1, T2 obj2, T3 obj3,
        T4 obj4)
    {
        return GetHashCode(obj1, GetHashCode(obj2, obj3, obj4));
    }

    /// <summary>
    /// Returns a hash code for the specified objects
    /// </summary>
    /// <param name="obj1">The first object.</param>
    /// <param name="obj2">The second object.</param>
    /// <param name="obj3">The third object.</param>
    /// <returns>
    /// A hash code, suitable for use in hashing algorithms and data 
    /// structures like a hash table. 
    /// </returns>
    public static int GetHashCode<T1, T2, T3>(T1 obj1, T2 obj2, T3 obj3)
    {
        return GetHashCode(obj1, GetHashCode(obj2, obj3));
    }

    /// <summary>
    /// Returns a hash code for the specified objects
    /// </summary>
    /// <param name="obj1">The first object.</param>
    /// <param name="obj2">The second object.</param>
    /// <returns>
    /// A hash code, suitable for use in hashing algorithms and data 
    /// structures like a hash table. 
    /// </returns>
    public static int GetHashCode<T1, T2>(T1 obj1, T2 obj2)
    {
        return GetHashCodeInternal(obj1.GetHashCode(), obj2.GetHashCode());
    }
}

以下是如何使用它的方法:

private struct Key
{
    private Type _type;
    private string _field;

    public Type Type { get { return _type; } }
    public string Field { get { return _field; } }

    public Key(Type type, string field)
    {
        _type = type;
        _field = field;
    }

    public override int GetHashCode()
    {
        return HashCodeHelper.GetHashCode(_field, _type);
    }

    public override bool Equals(object obj)
    {
        if (!(obj is Key))
            return false;
        var tf = (Key)obj;
        return tf._field.Equals(_field) && tf._type.Equals(_type);
    }
}

1
如何确定这些键?GetHashCode() 不需要任何参数,因此它需要以某种方式调用带有两个键的函数进行调用。很抱歉,没有进一步的解释,这看起来只是聪明,但并不是那么好。 - Michael Stum
4
如果你使用对象而不是泛型,你会得到装箱和内存分配,这在 GetHashCode 方法中是不希望发生的。因此,泛型是更好的选择。 - CodesInChaos
1
尾随的移位/异或步骤(h += (h << 10); h ^= (h >> 6); h += (h << 3); h ^= (h >> 11); h += (h << 15);)存在代码异味:它们不依赖于任何输入,看起来非常冗余。 - sehe
@nawfal 运行100万次大约需要390毫秒。运行Jon Skeet建议的解决方案100万次大约需要320毫秒,因此差别不是很大。 - Magnus
1
@Magnus 是的,你说得对,我会删除我的原始评论。只是想提醒一下,这可能不像其他解决方案那样快,但正如你所说,这并不重要。这个分发很棒,比大多数解决方案都好,所以我给它点赞! :) - nawfal
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,