Runtime: 建议:添加 System.HashCode,更容易生成好的哈希码。

创建于 2016-12-09  ·  182评论  ·  资料来源: dotnet/runtime

2017 年 6 月 16 日更新:寻找志愿者

API 形状已经完成。 但是,我们仍在从用于实现的候选列表中决定最佳哈希算法,我们需要有人帮助我们测量每个算法的吞吐量/分布。 如果您想担任该角色,请在下方发表评论, @karelz会将这个问题分配给您。

2017 年 6 月 13 日更新:已接受提案!

这是@terrajobsthttps://github.com/dotnet/corefx/issues/14354#issuecomment -308190321 上批准的 API:

// Will live in the core assembly
// .NET Framework : mscorlib
// .NET Core      : System.Runtime / System.Private.CoreLib
namespace System
{
    public struct HashCode
    {
        public static int Combine<T1>(T1 value1);
        public static int Combine<T1, T2>(T1 value1, T2 value2);
        public static int Combine<T1, T2, T3>(T1 value1, T2 value2, T3 value3);
        public static int Combine<T1, T2, T3, T4>(T1 value1, T2 value2, T3 value3, T4 value4);
        public static int Combine<T1, T2, T3, T4, T5>(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5);
        public static int Combine<T1, T2, T3, T4, T5, T6>(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6);
        public static int Combine<T1, T2, T3, T4, T5, T6, T7>(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6, T7 value7);
        public static int Combine<T1, T2, T3, T4, T5, T6, T7, T8>(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6, T7 value7, T8 value8);

        public void Add<T>(T value);
        public void Add<T>(T value, IEqualityComparer<T> comparer);

        [Obsolete("Use ToHashCode to retrieve the computed hash code.", error: true)]
        [EditorBrowsable(Never)]
        public override int GetHashCode();

        public int ToHashCode();
    }
}

该提案的原文如下。

基本原理

生成一个好的哈希码不应该需要在我们的代码上使用丑陋的魔法常量和位。 编写一个糟糕但简洁的GetHashCode实现应该不那么诱人,例如

class Person
{
    public override int GetHashCode() => FirstName.GetHashCode() + LastName.GetHashCode();
}

提议

我们应该添加一个HashCode类型来封装哈希码的创建,并避免迫使开发人员混淆在凌乱的细节中。 这是我的提议,它基于https://github.com/dotnet/corefx/issues/14354#issuecomment -305019329,并做了一些小的修改。

// Will live in the core assembly
// .NET Framework : mscorlib
// .NET Core      : System.Runtime / System.Private.CoreLib
namespace System
{
    public struct HashCode
    {
        public static int Combine<T1>(T1 value1);
        public static int Combine<T1, T2>(T1 value1, T2 value2);
        public static int Combine<T1, T2, T3>(T1 value1, T2 value2, T3 value3);
        public static int Combine<T1, T2, T3, T4>(T1 value1, T2 value2, T3 value3, T4 value4);
        public static int Combine<T1, T2, T3, T4, T5>(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5);
        public static int Combine<T1, T2, T3, T4, T5, T6>(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6);
        public static int Combine<T1, T2, T3, T4, T5, T6, T7>(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6, T7 value7);
        public static int Combine<T1, T2, T3, T4, T5, T6, T7, T8>(T1 value1, T2 value2, T3 value3, T4 value4, T5 value5, T6 value6, T7 value7, T8 value8);

        public void Add<T>(T value);
        public void Add<T>(T value, IEqualityComparer<T> comparer);
        public void AddRange<T>(T[] values);
        public void AddRange<T>(T[] values, int index, int count);
        public void AddRange<T>(T[] values, int index, int count, IEqualityComparer<T> comparer);

        [Obsolete("Use ToHashCode to retrieve the computed hash code.", error: true)]
        public override int GetHashCode();

        public int ToHashCode();
    }
}

评论

@terrajobst在的评论https://github.com/dotnet/corefx/issues/14354#issuecomment -305019329该API的目标; 他的所有言论都是有效的。 然而,我想特别指出这些:

  • API不需要生成强大的加密哈希
  • API 将提供“一个”哈希码,但不保证特定的哈希码算法。 这允许我们稍后使用不同的算法或在不同的架构上使用不同的算法。
  • API 将保证在给定进程内相同的值将产生相同的哈希码。 由于随机化,同一应用程序的不同实例可能会产生不同的哈希码。 这使我们能够确保消费者不能持久化哈希值并意外地依赖它们在运行(或更糟糕的是,平台版本)中的稳定性。
api-approved area-System.Numerics up-for-grabs

最有用的评论

决定

  • 我们应该删除所有AddRange方法,因为场景不清楚。 数组不太可能经常出现。 一旦涉及到更大的数组,问题是计算是否应该被缓存。 看到调用方的 for 循环清楚地表明您需要考虑这一点。
  • 我们也不希望将IEnumerable重载添加到AddRange因为它们会分配。
  • 我们认为我们不需要Add的重载,它需要stringStringComparison 。 是的,这些可能比通过IEqualityComparer调用更有效,但我们可以稍后解决这个问题。
  • 我们认为将GetHashCode标记为过时并带有错误是一个好主意,但我们会更进一步,同时也避开 IntelliSense。

这给我们留下了:

```C#
// 将存在于核心程序集中
// .NET 框架:mscorlib
// .NET Core : System.Runtime / System.Private.CoreLib
命名空间系统
{
公共结构哈希码
{
public static int 组合(T1值1);
public static int 组合(T1值1,T2值2);
public static int 组合(T1值1,T2值2,T3值3);
public static int 组合(T1值1,T2值2,T3值3,T4值4);
public static int 组合(T1值1、T2值2、T3值3、T4值4、T5值5);
public static int 组合(T1值1、T2值2、T3值3、T4值4、T5值5、T6值6);
public static int 组合(T1值1、T2值2、T3值3、T4值4、T5值5、T6值6、T7值7);
public static int 组合(T1值1、T2值2、T3值3、T4值4、T5值5、T6值6、T7值7、T8值8);

    public void Add<T>(T value);
    public void Add<T>(T value, IEqualityComparer<T> comparer);

    [Obsolete("Use ToHashCode to retrieve the computed hash code.", error: true)]
    [EditorBrowsable(Never)]
    public override int GetHashCode();

    public int ToHashCode();
}

}
``

所有182条评论

提案:添加哈希随机化支持

public static HashCode Randomized<T> { get; } // or CreateRandomized<T>
or 
public static HashCode Randomized(Type type); // or CreateRandomized(Type type)

TType type来获得相同类型的相同随机散列。

提案:添加对集合的支持

public HashCode Combine<T>(T[] values);
public HashCode Combine<T>(T[] values, IEqualityComparer<T> comparer);
public HashCode Combine<T>(Span<T> values);
public HashCode Combine<T>(Span<T> values, IEqualityComparer<T> comparer);
public HashCode Combine<T>(IEnumerable<T> values);
public HashCode Combine<T>(IEnumerable<T> IEqualityComparer<T> comparer);

我认为没有必要重载Combine(_field1, _field2, _field3, _field4, _field5)因为下一个代码HashCode.Empty.Combine(_field1).Combine(_field2).Combine(_field3).Combine(_field4).Combine(_field5);应该在没有组合调用的情况下进行内联优化。

@亚历克斯拉奇

提案:添加对集合的支持

是的,这是我对这个提案的最终计划的一部分。 不过,我认为在我们开始添加这些方法之前,关注我们希望 API 的样子很重要。

他想使用不同的算法,例如用于 coreclr 中字符串的 Marvin32 哈希。 这需要将 HashCode 的大小扩展到 8 个字节。

使用 Hash32 和 Hash64 类型在内部存储 4 或 8 个字节的数据怎么样? 记录每种方法的优缺点。 Hash64 对 X 有好处,但可能更慢。 Hash32 速度更快,但可能不是分布式(或实际上的权衡)。

他想随机化散列种子,因此散列不是确定性的。

这似乎是有用的行为。 但我可以看到人们想要控制这一点。 因此,也许应该有两种方法来创建哈希,一种不使用种子(并使用随机种子),另一种允许提供种子。

注意:如果可以在 Fx 中提供,Roslyn 会很高兴。 我们正在添加一个功能来为用户吐出一个 GetHashCode。 目前,它生成的代码如下:

c# public override int GetHashCode() { var hashCode = -1923861349; hashCode = hashCode * -1521134295 + this.b.GetHashCode(); hashCode = hashCode * -1521134295 + this.i.GetHashCode(); hashCode = hashCode * -1521134295 + EqualityComparer<string>.Default.GetHashCode(this.s); return hashCode; }

这不是一个很好的体验,它暴露了许多丑陋的概念。 我们会很高兴有一个 Hash.What 的 API,我们可以通过它来调用。

谢谢!

MurmurHash 呢? 它相当快并且具有非常好的散列特性。 还有两种不同的实现,一种输出 32 位哈希值,另一种输出 128 位哈希值。

还有 32 位和 128 位格式的矢量化实现。

这篇博文的声音来看, @tannergooding MurmurHash 速度很快,但并不安全。

@jkotas ,自从我们去年讨论以来,JIT 是否有任何工作围绕为 32 位 >4 字节结构生成更好的代码? 另外,您如何看待@CyrusNajmabadi的提议:

使用 Hash32 和 Hash64 类型在内部存储 4 或 8 个字节的数据怎么样? 记录每种方法的优缺点。 Hash64 对 X 有好处,但可能更慢。 Hash32 速度更快,但可能不是分布式(或实际上的权衡)。

我仍然认为这种类型对于提供给开发人员非常有价值,并且在 2.0 中拥有它会很棒。

@jamesqo ,我认为这个实现不需要加密安全(这是显式加密散列函数的目的)。

此外,该文章适用于 Murmur2。 该问题已在 Murmur3 算法中解决。

自从我们去年讨论以来,围绕 32 位 >4 字节结构生成更好代码的 JIT

我不知道任何。

你怎么看@CyrusNajmabadi的提议

框架类型应该是简单的选择,适用于 95% 以上的情况。 它们可能不是最快的,但没关系。 让您在 Hash32 和 Hash64 之间进行选择并不是一个简单的选择。

这对我来说没问题。 但是我们至少可以为那些 95% 的情况提供一个足够好的解决方案吗? 现在什么都没有... :-/

hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(this.s);

@CyrusNajmabadi为什么要在这里调用 EqualityComparer,而不仅仅是 this.s.GetHashCode()?

对于非结构:这样我们就不需要检查 null。

这也接近于我们在幕后为匿名类型生成的结果。 我优化了已知非空值的情况,以生成更令用户满意的代码。 但是如果有一个内置的 API 就好了。

对 EqualityComparer.Default.GetHashCode 的调用比检查 null... 的开销高 10 倍以上。

调用 EqualityComparer.Default.GetHashCode 比检查 null 贵 10 倍以上。

听起来是个问题。 如果只有好的哈希代码 API,我们可以在 Fx 中调用,我可以遵循:)

(此外,我们在匿名类型中也有这个问题,因为这也是我们在那里生成的)。

不确定我们对元组做了什么,但我猜它是相似的。

不确定我们对元组做了什么,但我猜它是相似的。

System.Tuple EqualityComparer<Object>.Default由于历史原因经历了System.ValueTuple使用空检查调用 Object.GetHashCode - https://github.com/dotnet/coreclr/blob/master/src/mscorlib/shared/System/ValueTuple.cs#L809。

不好了。 看起来元组只能使用“HashHelpers”。 是否可以将其公开以便用户可以获得相同的好处?

伟大的。 我很高兴做类似的事情。 我从我们的匿名类型开始,因为我认为它们是合理的最佳实践。 如果没有,那很好。 :)

但这不是我在这里的原因。 我来这里是为了获得一些实际上有效组合散列的系统。 如果/何时可以提供,我们将很乐意调用它,而不是使用随机数进行硬编码并自己组合哈希值。

您认为最适合编译器生成的代码的 API 形状是什么?

从字面上看,之前介绍的任何 32 位解决方案都适合我。 哎呀,64 位解决方案对我来说很好。 只是您可以获得的某种 API 说“我可以以某种合理的方式组合散列并产生合理分布的结果”。

我无法调和这些陈述:

我们有一个不可变的 HashCode 结构,大小为 4 个字节。 它有一个 Combine(int) 方法,该方法通过类似 DJBX33X 的算法将提供的哈希码与其自己的哈希码混合,并返回一个新的 HashCode。

@jkotas认为类似 DJBX33X 的算法不够健壮。

框架类型应该是简单的选择,适用于 95% 以上的情况。

我们能不能想出一个简单的 32 位累加散列,它可以在 95% 的情况下工作得很好? 哪些案件在这里处理得不好,为什么我们认为它们属于 95% 的情况?

@jkotas ,性能对于这种类型真的那么重要吗? 我认为平均而言,像哈希表查找这样的事情会比几个结构副本花费更多的时间。 如果事实证明这是一个瓶颈,那么在 API 发布后要求 JIT 团队优化 32 位结构副本是否合理,以便他们有一些动力,而不是在没有人致力于优化时阻止此 API副本?

我们能不能想出一个简单的 32 位累加散列,它可以在 95% 的情况下工作得很好?

默认情况下,我们已经严重烧毁了字符串的 32 位累积哈希,这就是为什么在 .NET Core 中为字符串使用 Marvin 哈希的原因 - https://github.com/dotnet/corert/blob/87e58839d6629b5f90777f886a2f52d7a99c076f/src/System。 src/系统/Marvin.cs#L25。 我认为我们不想在这里重复同样的错误。

@jkotas ,性能对于这种类型真的那么重要吗?

我不认为性能是关键。 由于看起来这个 API 将被自动生成的编译器代码使用,我认为我们应该更喜欢较小的生成代码而不是它的外观。 非流畅模式是较小的代码。

默认情况下,我们已经被严重烧毁了 32 位累积哈希字符串

这似乎不是 95% 的情况。 我们谈论的是普通开发人员只是想要一个“足够好”的散列,用于他们今天手动做事的所有类型。

由于看起来这个 API 将被自动生成的编译器代码使用,我认为我们应该更喜欢较小的生成代码而不是它的外观。 非流畅模式是较小的代码。

这不适用于 Roslyn 编译器。 当我们帮助用户为他们的类型生成 GetHashCodes 时,这是供 Roslyn IDE 使用的。 这是用户将看到并必须维护的代码,并且具有以下合理的内容:

```c#
返回 Hash.Combine(this.A?.GetHashCode() ?? 0,
this.B?.GetHashCode() ?? 0,
this.C?.GetHashCode() ?? 0);

is a lot nicer than a user seeing and having to maintain:

```c#
            var hashCode = -1923861349;
            hashCode = hashCode * -1521134295 + this.b.GetHashCode();
            hashCode = hashCode * -1521134295 + this.i.GetHashCode();
            hashCode = hashCode * -1521134295 + EqualityComparer<string>.Default.GetHashCode(this.s);
            return hashCode;

我的意思是,我们已经在 Fx 中有这个代码:

https://github.com/dotnet/roslyn/blob/master/src/Compilers/Test/Resources/Core/NetFX/ValueTuple/ValueTuple.cs#L5

我们认为这对于元组来说已经足够了。 我不清楚为什么将它提供给想要它用于自己类型的用户会出现这样的问题。

注意:我们甚至考虑过在 roslyn 中这样做:

c# return (this.A, this.B, this.C).GetHashCode();

但是现在你强迫人们生成一个(可能很大)结构只是为了获得某种合理的默认散列行为。

我们谈论的是普通开发人员只是想要一个“足够好”的散列,用于他们今天手动做事的所有类型。

原始字符串散列是一个“足够好”的散列,对于普通开发人员来说效果很好。 但是后来发现 ASP.NET 网络服务器容易受到 DoS 攻击,因为它们倾向于将接收到的内容存储在哈希表中。 所以“足够好”的哈希基本上变成了一个糟糕的安全问题。

我们认为这对于元组来说已经足够了

不一定。 我们为元组做了一个后退措施,使哈希码随机化,让我们可以选择稍后修改算法。

     return Hash.Combine(this.A?.GetHashCode() ?? 0,
                         this.B?.GetHashCode() ?? 0,
                         this.C?.GetHashCode() ?? 0);

这对我来说看起来很合理。

我不明白你的位置。 你好像在说两件事:

原始字符串散列是一个“足够好”的散列,对于普通开发人员来说效果很好。 但是后来发现 ASP.NET 网络服务器容易受到 DoS 攻击,因为它们倾向于将接收到的内容存储在哈希表中。 所以“足够好”的哈希基本上变成了一个糟糕的安全问题。

好的,如果是这样,那么让我们提供一个哈希码,它对有安全/DoS 问题的人有用。

框架类型应该是简单的选择,适用于 95% 以上的情况。

好的,如果是这样,那么让我们提供一个足以满足 95% 情况的哈希码。 有安全/DoS 问题的人员可以使用为此目的记录的专门表格。

不一定。 我们为元组做了一个后退措施,使哈希码随机化,让我们可以选择稍后修改算法。

好的。 我们可以公开它,以便用户可以使用相同的机制。

——
我在这里真的很挣扎,因为听起来我们在说“因为我们无法制定通用解决方案,每个人都必须推出自己的解决方案”。 这似乎是最糟糕的地方之一。因为当然,我们的大多数客户并没有考虑针对 DoS 问题推出他们自己的“marvin hash”。 他们只是添加、异或或以其他方式将字段哈希组合成一个最终哈希。

如果我们关心 95% 的情况,那么我们应该做一个总体上很好的 enogh 哈希。 如果我们关心 5% 的情况,我们可以为此提供专门的解决方案。

这对我来说看起来很合理。

太好了 :) 然后我们可以公开:

```c#
命名空间 System.Numerics.Hashing
{
内部静态类 HashHelpers
{
public static readonly int RandomSeed = new Random().Next(Int32.MinValue, Int32.MaxValue);

    public static int Combine(int h1, int h2)
    {
        // RyuJIT optimizes this to use the ROL instruction
        // Related GitHub pull request: dotnet/coreclr#1830
        uint rol5 = ((uint)h1 << 5) | ((uint)h1 >> 27);
        return ((int)rol5 + h1) ^ h2;
    }
}
Roslyn could then generate:

```c#
     return Hash.Combine(Hash.RandomSeed,
                         this.A?.GetHashCode() ?? 0,
                         this.B?.GetHashCode() ?? 0,
                         this.C?.GetHashCode() ?? 0);

这将具有在绝大多数情况下真正“足够好”的好处,同时也引导人们走上使用随机值初始化的良好道路,这样他们就不会依赖于非随机散列。

有安全/DoS 问题的人员可以使用为此目的记录的专门表格。

每个 ASP.NET 应用程序都有安全/DoS 问题。

太好了 :) 然后我们可以公开:

这不同于我所说的合理。

您如何看待https://github.com/aspnet/Common/blob/dev/shared/Microsoft.Extensions.HashCodeCombiner.Sources/HashCodeCombiner.cs 。 它是今天在 ASP.NET 内部在许多地方使用的东西,也是我非常满意的东西(除了组合功能需要更强大 - 我们可以不断调整的实现细节)。

@jkotas我听说:p

所以这里的问题是开发人员不知道他们什么时候容易受到 DoS 攻击,因为这不是他们关心的事情,这就是我们将字符串切换为使用 Marvin32 的原因。

我们不应该走“95% 的情况都无关紧要”的路线,因为我们无法证明这一点,即使有性能成本,我们也必须谨慎行事。 如果您打算远离它,那么哈希代码实现需要加密委员会审查,而不仅仅是我们决定“这看起来足够好”。

每个 ASP.NET 应用程序都有安全/DoS 问题。

好的。 那么你今天如何处理没有人对哈希码有任何帮助的问题,因此很可能做不好? 显然,拥有这种世界状态是可以接受的。 那么,提供一个合理的散列系统,其性能可能比今天人们手工滚动的系统更好,有什么害处呢?

因为我们没有办法证明这一点,即使它有性能成本,我们也必须谨慎行事

如果你不提供一些东西,人们就会继续做坏事。 拒绝“足够好”,因为没有什么是完美的,这意味着我们今天的现状很糟糕。

每个 ASP.NET 应用程序都有安全/DoS 问题。

你能解释一下吗? 据我了解,如果您接受任意输入,然后将其存储在某些数据结构中,如果输入可以特制,则性能不佳,您会遇到 DoS 问题。 好的,我知道这与用户在 Web 场景中获得的字符串有关。

那么这如何适用于在此场景中未使用的其余类型呢?

我们有这些类型的集合:

  1. 需要 DoS 安全的用户类型。 现在我们不提供任何帮助,所以我们已经处于一个糟糕的境地,因为人们可能没有做正确的事情。
  2. 不需要 DoS 安全的用户类型。 现在我们不提供任何帮助,所以我们已经处于糟糕的境地,因为人们可能没有做正确的事情。
  3. 需要 DoS 安全的框架类型。 现在我们已经使它们 DoS 安全,但我们不会通过 API 公开。
  4. 不需要 DoS 安全的框架类型。 现在我们已经给了他们哈希值,但我们不会通过 API 公开。

基本上,我们认为这些情况很重要,但还不够重要,无法实际为用户提供处理“1”或“2”的解决方案。 因为我们担心“2”的解决方案对“1”不利,所以我们甚至不会首先提供它。 如果我们甚至不愿意为“1”提供解决方案,那感觉就像我们处于一个非常奇怪的位置。 我们担心 DoSing 和 ASP,但并不担心是否能真正帮助人们。 因为我们不会在这方面帮助人们,所以我们甚至不愿意在非 DoS 情况下提供帮助。

——

如果这两种情况很重要(我愿意接受),那么为什么不只提供两个 API? 记录它们。 让他们清楚他们的用途。 如果人们正确使用它们,那就太好了。 如果人们没有正确使用它们,那仍然很好。 毕竟,他们很可能不是今天做的事情正确反正,怎么事情更糟?

你有什么想法

我没有任何意见。 如果它是一个客户可以使用的 API,它的性能可以接受,并且提供了一个简单的 API,并且在他们的终端上提供了清晰的代码,那么我认为这很好。

我认为拥有一个简单的静态表单来处理 99% 想要以有序方式组合一组字段/属性的情况会很好。 似乎可以相当简单地将这样的东西添加到这种类型中。

我认为有一个简单的静态表单会很好

同意。

我认为拥有一个简单的静态表单来处理 99% 想要以有序方式组合一组字段/属性的情况会很好。 似乎可以相当简单地将这样的东西添加到这种类型中。

同意。

我愿意在这个过程中与你们见面,因为我真的很想看到某种 API 出现。 @jkotas我仍然不明白您反对添加基于实例的不可变 API; 首先你说这是因为 32 位副本会很慢,然后因为可变 API 会更简洁(这不是真的; h.Combine(a).Combine(b) (不可变版本)比h.Combine(a); h.Combine(b); (可变版本))。

也就是说,我愿意回到:

public static class HashCode
{
    public static int Combine<T>(T value1, Tvalue2);
    public static int Combine<T>(T value1, Tvalue2, IEqualityComparer<T> comparer);
    public static int Combine<T>(T value1, Tvalue2, T value3);
    public static int Combine<T>(T value1, Tvalue2, T value3, IEqualityComparer<T> comparer);
    public static int Combine<T>(T value1, Tvalue2, T value3, T value4);
    public static int Combine<T>(T value1, Tvalue2, T value3, T value4, IEqualityComparer<T> comparer);
    // ... All the way until value8
}

这看起来合理吗?

我现在无法编辑我的帖子,但我刚刚意识到并非所有方法都可以接受 T。在这种情况下,我们可以只有 8 个重载接受所有整数并强制用户调用 GetHashCode。

如果这两种情况很重要(我愿意接受),那么为什么不只提供两个 API? 记录它们。 让他们清楚他们的用途。 如果人们正确使用它们,那就太好了。 如果人们没有正确使用它们,那仍然很好。 毕竟,无论如何,他们今天可能都做得不好,那么情况如何变得更糟?

因为人们在那里时不会正确使用东西。 我们举一个简单的例子,XSS。 从一开始,即使是 Web 表单也能够对输出进行 HTML 编码。 然而开发者不知道风险,不知道如何正确地做,直到发现为时已晚,他们的应用程序被发布了,哎呀,现在他们的 auth cookie 已经被取消了。

给人们一个安全选择假设他们

  1. 知道问题。
  2. 了解风险是什么。
  3. 可以评估这些风险。
  4. 可以很容易地发现正确的事情要做。

这些假设通常不适用于大多数开发人员,他们只有在为时已晚时才发现问题。 开发人员不参加安全会议,不阅读白皮书,也不了解解决方案。 因此,在 ASP.NET HashDoS 方案中,我们为它们做出了选择,我们默认保护它们,因为这是正确的做法,而且影响最大。 然而,我们只将它应用于字符串,这让那些根据用户输入构建自定义类的人处于不利的位置。 我们应该做正确的事情,现在就帮助保护那些客户,让它成为默认,有一个成功的坑,而不是失败的坑。 用于安全性的 API 设计有时不是关于选择,而是帮助用户,无论他们是否知道。

用户总是可以创建一个非安全的哈希; 所以给出两个选项

  1. 默认哈希实用程序是非安全意识的; 用户可以创建一个安全感知哈希函数
  2. 默认哈希实用程序具有安全意识; 用户可以创建自定义的非安全感知哈希函数

那么第二个可能更好; 并且建议的内容不会对加密哈希产生完整的性能影响; 所以这是一个很好的妥协?

这些线程中的一个运行问题是哪种算法对每个人都是完美的。 我认为可以肯定地说,没有一个完美的算法。 但是,我认为这不应该阻止我们提供比@CyrusNajmabadi所展示的代码更好的东西,它对于常见的 .NET 输入以及其他常见的散列器错误(例如丢失输入数据或容易可重置)。

我想提出几个选项来解决“最佳算法”问题:

  1. 显式选择:我计划很快针对一组非加密哈希(例如 xxHash、Marvin32 和 SpookyHash)发送 API 提案。 此类 API 的用法与 HashCode 或 HashCodeHelper 类型略有不同,但为了便于讨论,假设我们可以计算出这些差异。 如果我们为 GetHashCode 使用该 API:

    • 生成的代码明确说明了它在做什么——如果 Roslyn 生成Marvin32.Create(); ,它会让高级用户知道它决定做什么,如果他们愿意,他们可以轻松地将其更改为套件中的另一个算法。

    • 这意味着我们不必担心破坏更改。 如果我们从非随机化/差熵/慢算法开始,我们可以简单地更新 Roslyn 以开始在新代码中生成其他内容。 旧代码将继续使用旧哈希,新代码将使用新哈希。 开发人员(或 Roslyn 代码修复程序)可以根据需要更改旧代码。

    • 我能想到的最大缺点是,我们可能想要对 GetHashCode 进行的一些优化可能对其他算法有害。 例如,虽然 32 位内部状态与不可变结构很好地配合,但(比如说)CityHash 中的 256 位内部状态可能会浪费大量时间复制。

  1. 随机化:从适当的随机化算法开始( @CyrusNajmabadi显示的带有随机初始值的代码该站点显示 xxHash 在 x64 Mac 上最快,而 SpookyHash 在 Xbox 和 iPhone 上最快。 如果我们真的打算在某个时候改变算法,那么我们可能需要考虑设计一个 API,如果有 64 位以上的内部状态,它仍然具有合理的性能。

抄送@bartonjs , @terrajobst

@morganbr没有一个完美的算法,但我认为拥有一些在大多数情况下运行良好的算法,使用简单易懂的 API 公开是可以做的最有用的事情。 除此之外还有一套算法,用于高级用途是很好的。 但这不应该是唯一的选择,我不应该知道 Marvin 是谁,以便我可以将我的对象放入Dictionary

我不应该只知道 Marvin 是谁,这样我就可以将我的对象放入字典中。

我喜欢你这样说。 我也喜欢你提到字典本身。 IDictionary 是一种可以拥有大量具有各种不同质量的不同实现的东西(请参阅许多平台中的集合 API)。 然而,我们仍然只是提供一个基本的“词典”,它总体上做得不错,即使它可能不会在每个类别中都表现出色。

我认为这就是一的人都在哈希库中寻找。 可以完成工作的东西,即使它并不适合所有目的。

@morganbr我认为人们简单地想要一种编写 GetHashCode 的方法,它比他们今天所做的更好(通常是他们从网络上复制的一些数学运算的抓包组合)。 如果你能提供一个运行良好的基本实现,那么人们会很高兴。 如果高级用户对特定散列函数有强烈需求,那么您可以为他们提供一个幕后 API。

换句话说,今天编写哈希码的人不会知道或关心为什么他们会想要 Spooky vs Marvin vs Murmur。 只有对这些特定哈希码之一有特殊需求的人才会去寻找。 但是很多人需要说“这是我的对象的状态,为我提供一种生成分布良好的散列的方法,该散列速度很快,然后我可以与字典一起使用,我想这可以防止我在发生时被 DOS 攻击接受不受信任的输入并将其散列并存储”。

@CyrusNajmabadi问题是,如果我们将当前的兼容性概念扩展到未来,我们会发现这种类型一旦发布就永远不会改变(除非我们发现该算法以“它使所有应用程序都可攻击”的方式严重损坏) )。

曾经可以争辩说,如果它以稳定随机化的方式开始,那么更改实现就变得容易了,因为无论如何你都不能依赖于从运行到运行的值。 但是,如果几年后我们发现有一种算法可以提供即使不是更好的哈希桶平衡,而且具有更好的一般情况下的性能,但会产生一个涉及列表的结构\

Morgan 的建议是,您今天编写的代码将永远有效地具有相同的性能特征。 对于本可以变得更好的应用程序,这是不幸的。 对于本来会变得更糟的应用程序,这太棒了。 但是当我们找到新算法时,我们将其签入,然后我们更改 Roslyn(并建议更改 ReSharper/etc)以开始使用 NewAwesomeThing2019 而不是 SomeThingThatWasConsideredAwesomeIn2018 生成内容。

任何像这样的超级黑匣子只能完成一次。 然后我们永远坚持下去。 然后有人编写了下一个,它具有更好的平均性能,因此有两个黑盒实现,您不知道为什么要在它们之间进行选择。 然后……然后……

所以,当然,您可能不知道为什么 Roslyn/ReSharper/etc 使用 Marvin32、Murmur、FastHash 或基于 IntPtr.Size 的组合/条件为您自动编写 GetHashCode。 但是你有权力去调查它。 随着新信息的出现,您可以稍后在您的类型上更改它……但我们也赋予了您保持其不变的能力。 (如果我们写这个会很伤心,并且在 3 年内 Roslyn/ReSharper/etc 明确避免调用它,因为新算法好多了......通常)。

@bartonjs是什么让散列与 .Net 为您提供黑盒算法或数据结构的所有地方不同? 比如排序(introsort)、 Dictionary (基于数组的分离链)、 StringBuilder (8k块的链表)、LINQ的大部分。

我们今天对此进行了更深入的研究。 为延迟和在这个问题上的来回道歉。

要求

  • API 是为谁准备的?

    • API不需要生成强大的加密哈希

    • 但是:API需要足够好,以便我们可以在框架本身中使用它(例如在 BCL 和 ASP.NET 中)

    • 然而,这并不意味着我们必须在任何地方使用 API。 如果 FX 的某些部分出于安全/DOS 风险或出于性能的考虑,我们希望使用自定义的部分,那也没关系。 例外永远存在

  • 这个散列所需的属性是什么?

    • 使用输入中的所有位

    • 结果分布良好

    • API 将提供“一个”哈希码,但不保证特定的哈希码算法。 这允许我们稍后使用不同的算法或在不同的架构上使用不同的算法。

    • API 将保证在给定进程内相同的值将产生相同的哈希码。 由于随机化,同一应用程序的不同实例可能会产生不同的哈希码。 这使我们能够确保消费者不能持久化哈希值并意外地依赖它们在运行(或更糟糕的是,平台版本)中的稳定性。

API形状

```C#
// 将存在于核心程序集中
// .NET 框架:mscorlib
// .NET Core : System.Runtime / System.Private.CoreLib
命名空间系统
{
公共结构哈希码
{
public static int 组合(T1值1);
public static int 组合(T1值1,T2值2);
public static int 组合(T1值1,T2值2,T3值3);
public static int 组合(T1值1,T2值2,T3值3,T4值4);
public static int 组合(T1值1、T2值2、T3值3、T4值4、T5值5);
public static int 组合(T1值1、T2值2、T3值3、T4值4、T5值5、T6值6);
public static int 组合(T1值1、T2值2、T3值3、T4值4、T5值5、T6值6、T7值7);
public static int 组合(T1值1、T2值2、T3值3、T4值4、T5值5、T6值6、T7值7、T8值8);

    public void Add<T>(T value);
    public void Add<T>(T value, IEqualityComparer<T> comparer);
    public void Add<T>(T[] value);
    public void Add<T>(T[] value, int index, int length);
    public void Add(byte[] value);
    public void Add(byte[] value, int index, int length);
    public void Add(string value);
    public void Add(string value, StringComparison comparisonType);

    public int ToHashCode();
}

}

Notes:

* We decided to not override `GetHashCode()` to produce the hash code as this would be weird, both naming-wise as well as from a behavioral standpoint (`GetHashCode()` should return the object's hash code, not the one being computed).
* We decided to use `Add` for the builder patter and `Combine` for the static construction
* We decided to use not provide a static initialization method. Instead, `Add` will do this on first use.
* The struct is mutable, which is unfortunate but we feel the best compromise between making `GetHashCode()` very cheap & not cause any allocations while allowing the structure to be bigger than 32-bit so that the hash code algorithm can use more bits during accumulation.
* `Combine` will just call `<value>.GetHashCode()`, so it has the behavior of the value's type `GetHashCode()` implementation
    - For strings that means different casing will produce different hash codes
    - For arrays, that means the hash code doesn't look at the contents but uses reference semantics for the hash code
    - If that behavior is undesired, the developer needs to use the builder-style approach

### Usage

The simple case is when someone just wants to produce a good hash code for a given type, like so:

```C#
public class Customer
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public override int GetHashCode() => HashCode.Combine(Id, FirstName, LastName);
}

更复杂的情况是开发人员需要调整哈希的计算方式。 这个想法是调用站点传递所需的散列而不是对象/值,如下所示:

```C#
公共部分类客户
{
公共覆盖 int GetHashCode() =>
HashCode.Combine(
ID,
StringComparer.OrdinalIgnoreCase.GetHashCode(FirstName),
StringComparer.OrdinalIgnoreCase.GetHashCode(LastName),
);
}

And lastly, if the developer needs more flexibility, such as producing a hash code for more than eight values, we also provide a builder-style approach:

```C#
public partial class Customer
{
    public override int GetHashCode()
    {
        var hashCode = new HashCode();
        hashCode.Add(Id);
        hashCode.Add(FirstName, StringComparison.OrdinalIgnoreCase);
        hashCode.Add(LastName, StringComparison.OrdinalIgnoreCase);
        return hashCode.ToHashCode();
    }
}

下一步

这个问题将继续悬而未决。 为了实现 API,我们需要决定使用哪种算法。

@morganbr将为优秀的候选人提出建议。 一般来说,我们不想从头开始编写散列算法——我们想使用一个众所周知的,其属性很好理解的算法。

但是,我们应该衡量典型 .NET 工作负载的实现,看看哪种算法产生了良好的结果(吞吐量和分布)。 答案很可能因 CPU 架构而异,因此我们在测量时应考虑这一点。

@jamesqo ,你还有兴趣在这个领域工作吗? 在这种情况下,请相应地更新提案。

@terrajobst ,我们可能还想要public static int Combine<T1>(T1 value); 。 我知道它看起来有点有趣,但它会提供一种从输入哈希空间有限的东西中扩散位的方法。 例如,许多枚举只有几个可能的哈希值,只使用代码的底部几位。 一些集合建立在散列分布在更大空间的假设上,因此分散位可能有助于集合更有效地工作。

public void Add(string value, StrinComparison comparison);

Nit: StringComparison参数应该命名为comparisonType以匹配其他地方使用的命名方式StringComparison用作参数。

帮助我们选择算法的标准是:

  1. 算法是否有很好的雪崩效果? 也就是说,每一位输入是否都有 50% 的机会翻转每一位输出? 该站点研究了几种流行的算法。
  2. 小输入的算法速度快吗? 由于 HashCode.Combine 通常会处理 8 个或更少的整数,启动时间可能比吞吐量更重要。 这个网站有一组有趣的数据作为开始。 这也是我们可能需要针对不同架构或其他支点(OS、AoT 与 JIT 等)的不同答案的地方。

我们真正希望看到的是用 C# 编写的候选人的性能数据,以便我们可以合理地确信他们的特性将适用于 .NET。 如果你写了一个候选人而我们没有为此选择它,那么每当我真正为非加密哈希 API 收集 API 提案时,这仍然是有用的工作。

以下是我认为值得评估的一些候选人(但可以随意推荐其他人):

  • Marvin32(我们在这里已经有了 C# 实现)。 我们知道它对于 String.GetHashCode 来说足够快,并且我们相信它可以抵抗 HashDoS
  • xxHash32(这里是 x86 上最快的算法,根据 SMHasher 具有最高质量)
  • FarmHash(这里是 x64 上最快的。我还没有找到一个很好的质量指标。虽然这个可能很难用 C# 编写)
  • xxHash64(截断为 32 位)(这不是一个明显的速度赢家,但如果我们已经有了 xxHash32,可能很容易做到)
  • SpookyHash(倾向于在较大的数据集上表现良好)

可惜Add方法不能有ref HashCode的返回类型并返回ref this所以它们可以以流畅的方式使用,

readonly ref返回允许这样做吗? /cc @jaredpar @VSadov

警告:如果有人从互联网上某处的现有代码库中选择哈希实现,请保留源链接并检查许可证(我们也必须这样做)。

如果许可证不兼容,我们可能需要从头开始编写算法。

IMO,使用 Add 方法应该是非常罕见的。 它将用于非常高级的场景,并且不会真正存在“流利”的需求。

对于所有用户代码案例中 99% 的常见用例,应该能够使用=> HashCode.Combine(...)并没问题。

@morganbr

我们可能还想要public static int Combine<T1>(T1 value); 。 我知道它看起来有点有趣,但它会提供一种从输入哈希空间有限的东西中扩散比特的方法

有道理。 我已经添加了。

@justinvp

Nit: StringComparison参数应该命名为comparisonType以匹配其他地方使用的命名方式StringComparison用作参数。

固定的。

@CyrusNajmabadi

IMO,使用Add方法应该是非常罕见的。 它将用于非常高级的场景,并且不会真正存在“流利”的需求。

同意。

@benaadams - re: ref 从Add返回this - 不, this不能由结构方法中的 ref 返回,因为它可以是 rValue 或临时值。

```C#
ref var r = (new T()).ReturnsRefThis();

// r 在这里指的是某个变量。 哪一个? 范围/寿命是多少?
r = 其他东西();
``

如果它对比较有用,几年前我将Jenkins lookup3哈希函数( C 源代码)移植到 C# here

我想知道集合:

@terrajobst

c# public void Add<T>(T[] value);

为什么数组有重载,而一般集合没有重载(即IEnumerable<T> )?

此外, HashCode.Combine(array)hashCode.Add((object)array)以一种方式运行(使用引用相等)而hashCode.Add(array)以另一种方式运行(结合数组)?

@CyrusNajmabadi

对于 99% 的所有用户代码案例的常见用例,应该能够只使用=> HashCode.Combine(...)可以了。

如果目标真的是能够在 99% 的用例(而不是 80%)中使用Combine ,那么Combine不应该以某种方式支持基于值的散列集合在集合中? 也许应该有一个单独的方法来做到这一点( HashCode上的扩展方法或静态方法)?

如果 Add 是一个强大的场景,我们是否应该假设用户应该在 Object.GetHashCode 和组合集合的各个元素之间进行选择? 如果有帮助,我们可以考虑重命名数组(和潜在的 IEnumerable)版本。 就像是:
c# public void AddEnumerableHashes<T>(IEnumerable<T> enumerable); public void AddEnumerableHashes<T>(T[] array); public void AddEnumerableHashes<T>(T[] array, int index, int length);
我想知道我们是否还需要 IEqualityComparers 的重载。

建议:使构建器结构实现IEnumerable以支持集合初始值设定项语法:

C# return new HashCode { SomeField, OtherField, { SomeString, StringComparer.UTF8 }, { SomeHashSet, HashSet<int>.CreateSetComparer() } }.GetHashCode()

这比手动调用Add()优雅得多(特别是,您不需要临时变量),并且仍然没有分配。

更多细节

@SLaks也许更好的语法可以等待https://github.com/dotnet/csharplang/issues/455 (假设该提案有支持),这样HashCode就不必实现虚假的IEnumerable ?

我们决定不覆盖 GetHashCode() 来生成哈希码,因为这在命名和行为角度上都很奇怪(GetHashCode() 应该返回对象的哈希码,而不是正在计算的哈希码)。

我觉得奇怪的是GetHashCode不会返回计算出的哈希码。 我认为这会让开发人员感到困惑。 例如, @SLaks已经在他的提案中使用了它,而不是使用ToHashCode

@justinvp如果GetHashCode()不返回计算出的哈希码,它可能应该被标记为[Obsolete][EditorBrowsable(Never)]

另一方面,我没有看到返回计算出的哈希码的危害。

@terrajobst

我们决定不覆盖GetHashCode()来生成哈希码,因为这会很奇怪,无论是从命名还是从行为的角度来看( GetHashCode()应该返回对象的哈希码,而不是那个被计算)。

是的, GetHashCode()应该返回对象的哈希码,但是有什么理由说明这两个哈希码应该不同吗? 它仍然是正确的,因为具有相同内部状态的HashCode两个实例将从GetHashCode()返回相同的值。

@terrajobst我刚看到你的评论。 请原谅我的延迟回复,我查看通知的速度很慢,因为我认为它只会更加来回无处可去。 很高兴看到事实并非如此! :tada:

我很乐意接受它并进行吞吐量/分布测量(我认为这就是您所说的“有兴趣在该领域工作”的意思)。 不过,请给我一点时间来阅读这里的所有评论。

@terrajobst

我们能不能改变

public void Add<T>(T[] value);
public void Add<T>(T[] value, int index, int length);
public void Add(byte[] value);
public void Add(byte[] value, int index, int length);

public void AddRange<T>(T[] values);
public void AddRange<T>(T[] values, int index, int count);
public void AddRange<T>(T[] values, int index, int count, IEqualityComparer<T> comparer);

? 我重命名Add -> AddRange以避免@svick提到的行为。 我删除了byte重载,因为如果我们需要做任何特定于字节的事情,我们可以在方法中专门使用typeof(T) == typeof(byte) 。 另外,我改变了value -> valueslength -> count 。 有一个比较器重载也是有意义的。

@terrajobst你能提醒我为什么吗

        public void Add(string value);
        public void Add(string value, StringComparison comparisonType);

当我们有

        public void Add<T>(T value);
        public void Add<T>(T value, IEqualityComparer<T> comparer);

?

@svic

@justinvp如果 GetHashCode() 不返回计算出的哈希码,它可能应该被标记为 [Obsolete] 和 [EditorBrowsable(Never)]。

:+1:

@terrajobst我们可以回到从HashCode -> int进行隐式转换,所以没有ToHashCode方法吗? 编辑: ToHashCode很好。 请参阅下面的@CyrusNajmabadi的回复。

@jamesqo StringComparison是一个枚举。
但是,人们可以使用等效的StringComparer来代替。

我们可以回到从 HashCode -> int 进行隐式转换,所以没有 ToHashCode 方法吗?

我们讨论了这个问题,并在会议上决定反对。 问题在于,当用户获得最终的“int”时,通常会完成额外的工作。 即哈希码的内部结构通常会执行最终确定步骤,并且可能会将自身重置为新状态。 在隐式转换中发生这种情况会很奇怪。 如果你这样做:

HashCode hc = ...

int i1 = hc;
int i2 = hc;

那么你可能会得到不同的结果。

出于这个原因,我们也不喜欢显式转换(因为人们不认为转换是改变内部状态)。

使用一种方法,我们可以明确地记录这种情况的发生。 我们甚至可以命名它以尽可能多地传达它。 即“ToHashCodeAndReset”(虽然我们决定反对)。 但至少该方法可以有明确的文档,用户可以在诸如智能感知之类的东西中看到。 转换的情况并非如此。

我删除了字节重载,因为我们可以专门使用 typeof(T) == typeof(byte)

IIRC 有人担心从 JIT 的角度来看这不合适。 但这可能仅适用于非值类型的“typeof()”情况。 只要 jit 能够有效地为值类型 typeof() 情况做正确的事情,那就应该是好的。

@CyrusNajmabadi我不知道转换为int可能涉及变异状态。 ToHashCode就是这样。

对于那些考虑加密观点的人 - http://tuprints.ulb.tu-darmstadt.de/2094/1/thesis.lehmann.pdf

@terrajobst ,您是否有时间阅读我的评论(从这里开始)并决定您是否同意调整后的 API 形状? 如果是这样,那么我认为这可以标记为 api-approved/up for grass,我们可以开始决定哈希算法。

@blowdart ,您想强调的任何特定部分?

上面我可能不太明确,但我不知道 HashDoS 入侵的唯一非加密哈希是 Marvin 和 SipHash。 也就是说,即使播种(比如说)带有随机值的 Murmur仍然可以被破坏并用于 DoS。

没有,我只是觉得它很有趣,我认为这方面的文档应该说“不适用于通过加密算法生成的哈希码”。

决定

  • 我们应该删除所有AddRange方法,因为场景不清楚。 数组不太可能经常出现。 一旦涉及到更大的数组,问题是计算是否应该被缓存。 看到调用方的 for 循环清楚地表明您需要考虑这一点。
  • 我们也不希望将IEnumerable重载添加到AddRange因为它们会分配。
  • 我们认为我们不需要Add的重载,它需要stringStringComparison 。 是的,这些可能比通过IEqualityComparer调用更有效,但我们可以稍后解决这个问题。
  • 我们认为将GetHashCode标记为过时并带有错误是一个好主意,但我们会更进一步,同时也避开 IntelliSense。

这给我们留下了:

```C#
// 将存在于核心程序集中
// .NET 框架:mscorlib
// .NET Core : System.Runtime / System.Private.CoreLib
命名空间系统
{
公共结构哈希码
{
public static int 组合(T1值1);
public static int 组合(T1值1,T2值2);
public static int 组合(T1值1,T2值2,T3值3);
public static int 组合(T1值1,T2值2,T3值3,T4值4);
public static int 组合(T1值1、T2值2、T3值3、T4值4、T5值5);
public static int 组合(T1值1、T2值2、T3值3、T4值4、T5值5、T6值6);
public static int 组合(T1值1、T2值2、T3值3、T4值4、T5值5、T6值6、T7值7);
public static int 组合(T1值1、T2值2、T3值3、T4值4、T5值5、T6值6、T7值7、T8值8);

    public void Add<T>(T value);
    public void Add<T>(T value, IEqualityComparer<T> comparer);

    [Obsolete("Use ToHashCode to retrieve the computed hash code.", error: true)]
    [EditorBrowsable(Never)]
    public override int GetHashCode();

    public int ToHashCode();
}

}
``

后续步骤:问题是抢手——用几个候选算法作为实验https://github.com/dotnet/corefx/issues/14354#issuecomment -305028686 列表,这样我们就可以决定采用哪种算法(基于吞吐量和分布测量,每个 CPU 架构可能会有不同的答案)。

复杂度:大

如果有人有兴趣拿起它,请ping我们。 甚至可能有几个人一起工作的空间。 ( @jamesqo你有优先选择,因为你在这个问题上投入最多和最长)

@karelz尽管我有上述评论,但我改变了主意,因为我认为我没有资格选择最佳哈希算法。 我查看了@morganbr列出的一些库,并意识到实现非常复杂,因此我无法轻松将其转换为 C# 进行测试。 我在 C++ 方面的背景很少,所以我也很难仅仅安装库和编写测试应用程序。

不过,我不希望它永远留在抢购名单上。 如果从今天起一周没有人接受它,我会考虑在 Programmers SE 或 Reddit 上发布一个问题。

我没有对其进行测试(或以其他方式对其进行优化),但这是我在几个个人项目中使用的 Murmur3 哈希算法的基本实现: https ://gist.github.com/tannergooding/0a12559d1a912068b9aeb4b9586aad7f

我觉得这里最好的解决方案是根据输入数据的大小动态更改哈希算法。

例如:Mumur3(和其他)对于大型数据集非常快,并且提供了很好的分布,但是对于较小的数据集,它们可能“很差”(速度方面,而不是分布方面)。

我想我们应该这样做:如果总字节数小于 X,则执行算法 A; 否则,执行算法 B。这仍然是确定性的(每次运行),但将允许我们根据输入数据的实际大小提供速度和分布。

可能还值得注意的是,提到的几种算法都有专门为 SIMD 指令设计的实现,因此性能最高的解决方案可能会涉及某个级别的 FCALL(就像使用某些 BufferCopy 实现一样)或可能涉及依赖在System.Numerics.Vector

@jamesqo ,我们很乐意帮助您做出选择; 我们最需要帮助的是候选实现的性能数据(理想情况下是 C#,尽管正如@tannergooding指出的那样,某些算法需要特殊的编译器支持)。 正如我上面提到的,如果你构建了一个没有被选中的候选人,我们以后可能会使用它,所以不要担心工作会被浪费。

我知道有各种实现的基准,但我认为使用此 API 和可能的输入范围(例如具有 1-10 个字段的结构)进行比较很重要。

@tannergooding ,这种适应性可能是最

此外,考虑到最可能的输入范围是 4-32 字节( Combine`1 - Combine`8 ),希望在该范围内不会有太大的性能变化。

这种适应性可能是最高效的,但我不知道它如何与 Add 方法一起工作,因为它不知道它会被调用多少次。

我个人并不相信 API 形状非常适合通用散列(但是它很接近)......

目前我们公开了Combine静态构造方法。 如果这些是为了组合所有输入并生成最终的哈希码,那么名称是“糟糕的”,像Compute这样的东西可能更合适。

如果我们公开Combine方法,它们应该只混合所有输入,并且应该要求用户调用Finalize方法,该方法获取最后组合的输出以及所用的总字节数结合以产生最终的哈希码(最终确定哈希码很重要,因为它是导致比特雪崩的原因)。

对于构建器模式,我们公开了AddToHashCode方法。 目前尚不清楚Add方法是否旨在存储字节并且仅在对ToHashCode的调用时组合/完成(在这种情况下,我们可以动态选择正确的算法),或者它们是否是意味着要动态组合,应该清楚情况是这样(并且实现应该在内部跟踪组合的总字节大小)。

对于寻找不太复杂的起点的任何人,请尝试 xxHash32。 这很可能很容易转换为 C#(人们已经做到了)。

仍在本地测试,但我看到我的 Murmur3 的 C# 实现的以下吞吐率。

这些用于 1-8 个输入的静态组合方法:

1070.18 mb/s
1511.49 mb/s
1674.89 mb/s
1957.65 mb/s
2083.24 mb/s
2140.94 mb/s
2190.27 mb/s
2245.53 mb/s

我的实现假设应该为每个输入调用GetHashCode并且计算值应该在返回之前完成。

我组合了int值,因为它们是最容易测试的。

为了计算吞吐量,我运行了 10,001 次迭代,将第一次迭代作为“热身”运行丢弃。

在每次迭代中,我运行 10,000 次子迭代,我调用HashCode.Combine ,将前一个子迭代的结果作为下一次迭代的第一个输入值传递。

然后我平均所有迭代以获得平均经过时间,进一步除以每个循环运行的子迭代次数以获得每次调用的平均时间。 然后我计算每秒可以进行的调用数,并将其乘以组合的字节数以计算实际吞吐量。

将清理代码并稍后分享。

@tannergooding ,听起来进步很大。 为确保获得正确的测量结果,API 的目的是调用HashCode.Combine(a, b)等效于调用

HashCode hc = new HashCode();
hc.Add(a); // Initializes the hash state, calls a.GetHashCode() and feeds the result into the hash state
hc.Add(b); // Calls b.GetHashCode() and feeds the result into the hash state
return hc.ToHashCode(); // Finalizes the hash state, truncates it to an int, resets the internal state and returns the int

在这两种情况下,数据都应该进入相同的内部散列状态,并且散列应该在最后确定一次。

👍

这实际上是我编写的代码正在做的事情。 唯一的区别是我有效地内联了所有代码(不需要分配new HashCode()并跟踪组合的字节数,因为它是常量)。

@morganbr。 Murmur3 的实现 + 吞吐量测试: https ://gist.github.com/tannergooding/89bd72f05ab772bfe5ad3a03d6493650

MurmurHash3 基于此处描述的算法: https :

致力于 xxHash32(BSD-2 条款——https://github.com/Cyan4973/xxHash/blob/dev/xxhash.c)和 SpookyHash(公共领域——http://www.burtleburtle.net/bob/hash /spooky.html) 变体

@tannergooding同样,不是哈希专家,但我记得 [阅读文章] [1] 说 Murmur 不抗 DoS,所以在我们选择之前指出这一点。

@jamesqo ,我可能是错的,但我相当确定该漏洞适用于 Murmur2 而不是 Murmur3。

在任何一种情况下,我都在实现几种算法,以便我们可以获得 C# 的吞吐量结果。 这些算法的分布和其他特性是众所周知的,所以我们可以稍后选择哪个是最好的 😄

哎呀,忘了链接到文章: http :

@tannergooding好的。 听起来很公平:+1:

@tannergooding ,我查看了您的 Murmur3 实现,它通常看起来不错,并且可能优化得很好。 为了确保我理解正确,您是否使用了 combineValue 和 Murmur 的内部状态都是 32 位的事实? 对于这种情况,这可能是一个很好的优化,并解释了我之前的一些困惑。

如果我们采用它,它可能需要进行一些调整(尽管它们可能不会对性能测量产生太大影响):

  • 结合仍应在 value1 上调用 CombineValue
  • 第一次 CombineValue 调用应该采用随机种子
  • ToHashCode 应该重置 _bytesCombined 和 _combinedValue

与此同时,虽然我渴望这个 API,但通过(field1, field2, field3).GetHashCode()实现 GetHashCode 对我来说有多糟糕?

@jnm2 , ValueTuple 哈希码组合器倾向于将您的输入按顺序排列在哈希码中(并丢弃最近的输入)。 对于几个字段和一个除以质数的哈希表,您可能不会注意到。 对于许多字段或除以 2 的幂的哈希表,您插入的最后一个字段的熵将对您是否发生冲突的影响最大(例如,如果您的最后一个字段是 bool 或小整数,您' 可能会有很多冲突,如果它是一个 guid,你可能不会)。

ValueTuple 也不适用于全为 0 的字段。

附带说明一下,我不得不停止处理其他实现(具有更高优先级的工作)。 不知道什么时候才能捡回来。

因此,如果这对于结构化类型来说还不够好,那为什么对于元组来说就足够了呢?

@jnm2 ,这是该功能值得构建的原因之一——因此我们可以在整个框架中替换不合标准的哈希。

具有性能和质量特征的大型哈希函数表:
https://github.com/leo-yuriev/t1ha

@arespr我认为团队正在寻找哈希函数的 C# 实现。 不过还是谢谢分享。

@tannergooding你仍然无法解决这个问题吗? 如果是这样,那么我将在 Reddit/Twitter 上发布我们正在寻找哈希专家。

编辑:在 Reddit 上发帖。 https://www.reddit.com/r/csharp/comments/6qsysm/looking_for_hash_expert_to_help_net_core_team/?ref=share&ref_source=link

@jamesqo ,我的

此外,当前的测量将受到我们当前可以在 C# 中编码的内容的限制,但是,如果/当这成为一件事 (https://github.com/dotnet/designs/issues/13),测量可能会有所改变;)

此外,当前的测量将受到我们当前可以在 C# 中编码的内容的限制,但是,如果/当这成为一件事 (dotnet/designs#13),测量可能会有所改变;)

没关系 - 一旦内在函数可用,我们总是可以更改哈希算法,封装/随机化哈希代码使我们能够做到这一点。 我们只是在寻找能够在当前状态下为运行时提供最佳性能/分布权衡的东西。

@jamesqo ,感谢您寻找帮助的人。 我们很高兴有一个不是哈希专家的人在这方面工作——我们真的只需要一个可以将一些算法从其他语言或设计移植到 C# 的人,然后进行性能测量。 一旦我们选择了候选人,我们的专家就会对任何更改做我们所做的事情——检查代码的正确性、性能、安全性等。

你好! 我刚刚通读了讨论,至少在我看来,这个案例似乎已经非常支持 murmur3-32 PoC。 BTW 对我来说似乎是一个非常好的选择,我建议不要再花费任何不必要的工作(但甚至可能放弃.Add()会员......)。

但在不太可能的情况下,有人想继续进行更多的性能工作,我可以为 xx32、xx64、hsip13/24、seahash、murmur3-x86/32(我从上面集成了 marvin32 impl)和(还)提供一些代码未优化)sip13/24,spookyv2。 如果需要,某些版本的 City 看起来很容易移植。 那个半废弃的项目考虑到了一个稍微不同的用例,所以没有带有提议 API 的 HashCode 类; 但对于基准测试来说,它应该无关紧要。

Definitly不生产就绪:代码适用大方量的像复制面食,积极的内联和不安全的癌细胞蔓延蛮力; 字节序不存在,未对齐的读取也不存在。 甚至针对 ref-impl 测试向量的测试委婉地说是“不完整的”。

如果这有任何帮助,我应该在接下来的两周内找到足够的时间来解决最严重的问题,并提供代码和一些初步结果。

@gimpf

我刚刚通读了讨论,至少在我看来,这个案例似乎已经非常支持 murmur3-32 PoC。 BTW 对我来说似乎是一个非常好的选择,我建议不要再花任何不必要的工作

不,人们还不喜欢 Murmur3。 我们希望确保在性能/分布之间的平衡方面选择绝对最佳的算法,因此我们不能不遗余力。

但在不太可能的情况下,有人想继续进行更多的性能工作,我可以为 xx32、xx64、hsip13/24、seahash、murmur3-x86/32(我从上面集成了 marvin32 impl)和(还)提供一些代码未优化)sip13/24,spookyv2。 如果需要,某些版本的 City 看起来很容易移植。

是的,请! 我们希望为尽可能多的算法收集代码以进行测试。 您可以贡献的每一个新算法都是有价值的。 如果您也可以移植 City 算法,将不胜感激。

绝对不是生产就绪的:代码应用了大量的蛮力,如复制意大利面、侵略性内联和不安全的恶性蔓延; 字节序不存在,未对齐的读取也不存在。 甚至针对 ref-impl 测试向量的测试委婉地说是“不完整的”。

没关系。 只需将代码带入,如果需要,其他人可以找到它。

如果这有任何帮助,我应该在接下来的两周内找到足够的时间来解决最严重的问题,并提供代码和一些初步结果。

是啊,那样最好了!

@jamesqo好的,一旦我有东西要展示,我就会

@gimpf听起来真的很棒,我们很想听听您的进度(无需等到您完成每个算法!)。 只要您相信代码会产生正确的结果并且性能很好地代表了我们在生产就绪的实现中所看到的情况,那么非生产就绪就可以了。 一旦我们选择了候选人,我们就可以与您一起进行高质量的实施。

我还没有看到关于 seahash 的熵与其他算法相比如何的分析。 你有任何指示吗? 它具有听起来很有趣的性能权衡……矢量化听起来很快,但模块化算术听起来很慢。

@morganbr我已经准备好了预告片。

关于 SeaHash :不,我还不知道质量; 如果性能很有趣,我会将其添加到 SMHasher。 至少作者声称它很好(将它用于文件系统中的校验和),并且还声称在混合过程中没有丢弃任何熵。

关于哈希值和基准测试:项目Haschisch.Kastriert ,维基页面比较 xx32、xx64、hsip13、hsip24、marvin32、sea 和 murmur3-32 的第

一些重要的警告:

  • 这是一个非常快速的台架运行,精度设置较低。
  • 实现还没有真正完成,一些竞争者仍然缺失。 Streaming 实现(这样的事情对于合理的 .Add() 支持来说是必要的)需要实际优化。
  • SeaHash 目前没有使用种子。

第一印象:

  • 对于大消息,xx64 是列出的实现中最快的(据我所知,每个周期大约 3.25 个字节,或者在我的笔记本上为 9.5 GiB/s)
  • 对于短消息,没有什么是好的,但是 murmur3-32 和(令人惊讶的) seahash 有优势,但后者可能是由 seahash 尚未使用种子来解释的。
  • 访问HashSet<>的“基准”需要工作,因为一切几乎都在测量误差范围内(我已经看到了更大的差异,但仍然不值得讨论)
  • 当组合哈希码时,murmur-3A PoC 比我们这里的快 5 到 20 倍
  • C# 中的一些抽象非常昂贵; 这使得比较哈希算法比必要的更烦人。

一旦我的情况有所改善,我会再次给你写信。

@gimpf ,这是一个美妙的开始! 我看了一下代码和结果,我有几个问题。

  1. 您的结果显示 SimpleMultiplyAdd 比@tannergooding的 Murmur3a 慢约 5 倍。 这似乎很奇怪,因为 Murmur 要做的工作比乘法+加法还多(尽管我承认旋转比加法运算更快)。 您的实现是否有可能在 Murmur 实现中没有普遍的低效率,或者我应该将其理解为自定义实现比通用实现有很大优势?
  2. 有 1、2 和 4 种组合的结果很好,但是这个 API 最多可以达到 8 个。是否也可以获得结果,或者这是否会导致过多的重复?
  3. 我看到您在 X64 上运行,所以这些结果应该有助于我们选择我们的 X64 算法,但其他基准测试表明 X86 和 X64 之间的算法可能会有很大差异。 你也容易得到X86结果吗? (在某些时候,我们还需要获得 ARM 和 ARM64,但这些肯定可以等待)

您的 HashSet 结果特别有趣。 如果他们坚持下去,那可能是更喜欢更好的熵而不是更快的散列时间。

@morganbr这个周末更断断续续,所以进展有限。

关于您的问题:

  1. 您的结果显示 SimpleMultiplyAdd 比@tannergooding的 Murmur3a 慢约 5 倍。 这似乎很奇怪......

我在想自己。 这是一个复制/粘贴错误,SimpleMultiplyAdd 总是组合四个值......此外,通过重新排序一些语句,乘加组合器稍微快了一点(吞吐量提高了约 60%)。

您的实现是否有可能在 Murmur 实现中没有普遍的低效率,或者我应该将其理解为自定义实现比通用实现有很大优势?

我可能会错过一些东西,但对于 .NET 通用实现似乎不适用于此用例。 我已经为所有算法编写了组合式方法,并且 wrt 哈希代码组合的大多数性能_比通用方法好得多。

然而,即使是这些实现仍然太慢; 需要进一步的工作。 .NET 在这方面的性能对我来说是绝对不透明的; 添加或删除局部变量的副本可以轻松地将性能提高两倍。 为了选择最佳选项,我可能无法提供经过充分优化的实现。

  1. 具有 1、2 和 4 组合的结果很好,但此 API 最多可达 8。

我已经扩展了组合基准。 在这方面没有惊喜。

  1. 看到你跑的是X64(...),你也容易得到X86的结果吗?

曾经是,但后来我移植到 .NET Standard。 现在我陷入了依赖地狱,只有 .NET Core 2 和 CLR 64 位基准测试有效。 一旦我解决了当前的问题,这可以很容易地解决。

你认为这会在 v2.1 版本中实现吗?

@gimpf你有一段时间没有发帖了——你有关于你的实现的进展更新吗? :笑脸:

@jamesqo我修复了一些导致奇怪结果的基准,并将 City32、SpookyV2、Sip13 和 Sip24 添加到可用算法列表中。 Sips 与预期的一样快(相对于 xx64 的吞吐量),City 和 Spooky 没有(对于 SeaHash 仍然如此)。

对于组合哈希码,Murmur3-32 看起来仍然是一个不错的选择,但我还没有进行更详尽的比较。

另一方面,流 API (.Add()) 具有从候选列表中删除一些哈希算法的不幸副作用。 鉴于此类 API 的性能也存在问题,您可能需要重新考虑是否从一开始就提供它。

如果避免.Add()部分,并且考虑到哈希组合器正在使用种子,我认为清理 tg 的组合器、创建小型测试套件不会有任何危害,并且今天就这样吧。 由于我每个周末只有几个小时,而且性能优化有些乏味,所以镀金版本可能会拖延一点......

@gimpf ,这听起来进步很大。 您手头是否有结果表,以便我们可以查看是否有足够的数据来做出决定并继续前进?

@morganbr我已经更新了我的基准测试结果

目前,我在 .NET Core 2 上只有 64 位结果。对于该平台,City64 w/o seed 是所有规模中最快的。 结合一粒种子,XX-32 与 Murmur-3-32 并列。 幸运的是,这些算法在 32 位平台上享有快速的声誉,但显然我们需要验证这也适用于我的实现。 结果似乎代表了真实世界的性能,除了 Sea 和 SpookyV2 似乎异常缓慢。

您将需要考虑您真正需要多少哈希代码组合器的哈希保护。 如果只需要播种以使散列明显无法用于持久性,则 city64 一旦与 32 位种子异或将是一种改进。 由于此实用程序仅用于组合散列(而不是替换例如字符串的散列代码,或作为整数数组的插入式散列器等),这可能已经足够了。

如果您认为需要 OTOH,您会很高兴看到 Sip13 通常比 XX-32(在 64 位平台上)慢不到 50%,但对于 32 位应用程序,结果可能会有很大不同。

不知道它与 corefx 有多大关系,但我添加了 LegacyJit 32bit(w/FW 4.7)结果。

我想说的是,结果慢得可笑。 但是,举个例子,在 56 MiB/s 与 319 MiB/s 之间,我并没有笑(那是 Sip,它最缺少左旋转优化)。 我想我记得为什么我在一月份取消了我的 .NET 哈希算法项目......

所以,RyuJit-32bit 仍然缺失,并且(希望)会给出非常不同的结果,但对于 LegacyJit-x86,Murmur-3-32 轻松获胜,只有 City-32 和 xx-32 可以接近。 Murmur 的性能仍然很差,只有大约 0.4 到 1.1 GB/s,而不是 0.6 到 2 GB/s(在同一台机器上),但至少它在正确的范围内。

今晚我将在我的一些机器上运行基准测试并发布结果(Ryzen、i7、Xeon、A10、i7 Mobile,我认为还有其他一些)。

@tannergooding @morganbr一些不错的重要更新。

首先重要的是:

  • 我修复了一些产生不正确哈希值的组合实现。
  • 基准套件现在更加努力地避免不断折叠。 City64 易受感染(过去的 murmur-3-32 也是如此)。 并不意味着我现在了解每个结果,但它们更合理。

好东西:

  • 组合器实现现在可用于所有 1 到 8 个参数重载,包括 xx/city 的稍微麻烦的手动展开实现。
  • 测试和基准测试也会检查这些。 由于许多散列算法具有特殊情况的低字节消息,因此这些测量可能会引起人们的兴趣。
  • 多个目标的简化运行基准(核心与固件)。

要在所有主要实现上运行套件以组合哈希码,包括“Empty”(纯开销)和“multiply-add”(著名 SO 答案的速度优化版本):

bin\Release\net47\Haschisch.Benchmarks.Net47.exe -j:clr_x86 -j:clr_x64_legacy -j:clr_x64 -j:core_x64 -- CombineHashCode --allcategories=prime

(_运行 32 位核心基准测试似乎很方便,似乎需要预发布的 BenchmarkDotNet(或者可能是仅 32 位的设置加上使用基于核心的基准运行程序)。然后它应该可以使用 -j:core_x86,希望如此)_

结果:在所有错误修复之后,xx32 似乎在所有带有 64 位 RyuJIT 的重载中获胜,在移动 Haswell i7 上的 Windows 10 上,在“快速”运行中。 在 Sips 和 marvin32 之间,Sip-1-3 总是获胜。 Sip-1-3 比 xx32 慢约 4 倍,而 xx32 又比原始乘加组合器慢约 2 倍。 32 位核心结果仍然缺失,但我或多或少在等待一个稳定的 BenchmarkDotNet 版本,它将为我解决这个问题。

(编辑)我刚刚添加了一个用于访问 hash-set比μ-基准以上细节更加依赖,但你可能想给它看看。

再次感谢@gimpf提供的精彩数据! 让我们看看我们是否可以把它变成一个决定。

首先,我将这样划分算法:
快+好熵(按速度排序):

  1. xxHash32
  2. City64(这在 x86 上可能会很慢,所以我们可能不得不为 x86 选择其他东西)
  3. 杂音3A

HashDoS 抗性:

  • 马文32
  • 哈希。 如果我们倾向于这一点,我们需要让微软的加密专家对其进行审查,以确认研究结果是可以接受的。 我们还必须弄清楚哪些参数足够安全。 该论文建议介于 Sip-2-4 和 Sip-4-8 之间。

非争用(慢):

  • 幽灵V2
  • 城市32
  • xxHash64
    *SeaHash(我们没有熵的数据)

不竞争(坏熵):

  • 乘加
  • HSIP

在我们选择获胜者之前,我想确保其他人同意我上面的观点。 如果它成立,我认为我们只需要选择是否为 HashDoS 抵抗支付 2 倍,然后按速度进行。

@morganbr您的分组看起来不错。 作为 SipHash 轮次中的一个数据点,Rust 项目询问了Jean-Philippe Aumasson ,他是 sip-hash w/DJB 的作者。 在那次讨论之后,他们决定使用 sip-1-3 来处理哈希表。

(参见PR rust:#33940和随附的问题 rust:#29754 )。

根据数据和评论,我想建议我们在所有架构上使用xxHash32 。 下一步是让它实施。 @gimpf ,你有兴趣

对于那些关心 HashDoS 的人,我将很快跟进一个关于通用哈希 API 的提案,该 API 应该包括 Marvin32 并且可能包括 SipHash。 这也将是@gimpf@tannergooding已经处理过的其他实现的合适位置。

@morganbr我可以在时间允许的情况下整理一个 PR。 另外,我个人也更喜欢 xx32,只要它不会降低接受度。

@gimpf ,你的时间怎么样? 如果你真的没有时间,我们也可以看看有没有其他人愿意试一试。

@morganbr我原计划在 11 月 5 日之前完成它,但看起来仍然不错,我会在接下来的两周内找到时间。

@gimpf ,听起来不错。 感谢更新!

@terrajobst - 我参加聚会有点晚了(抱歉),但我们不能更改 Add 方法的返回类型吗?

```c#
公共哈希码添加(T值);
公共哈希码添加(T 值,IEqualityComparer比较器);

The params code is clearly there for scenarios where you have multiple fields, e.g.

```c#
        public override int GetHashCode() => new HashCode().Add(Name, Surname).ToHashCode();

但是,可以像这样实现完全相同的事情,尽管减少了一种浪费的数组分配:

c# public override int GetHashCode() => new HashCode().Add(Name).Add(Surname).Add(Age).ToHashCode();

请注意,类型也可以混合使用。 这显然可以通过不在常规方法中流畅地调用它来完成。 鉴于流畅接口并非绝对必要的论点,为什么一开始就存在浪费的params重载? 如果这个建议是一个坏建议,那么params重载就落到了同样的轴上。 那,并为一个琐碎但最佳的哈希码强制使用常规方法似乎是一种仪式。

编辑: implicit operator int对 DRY 也很好,但并不完全重要。

@jcdickinson

我们不能更改 Add 方法的返回类型吗?

我们已经在旧提案中讨论过这个问题,但它被拒绝了。

为什么一开始就存在浪费的参数重载?

我们没有添加任何参数重载? 在此网页上对“params”执行 Ctrl+F,您将看到您的评论是该词弹出的唯一位置。

隐式运算符 int 也适用于 DRY,但并不完全重要。

我相信这也在上面的某个地方讨论过......

@jamesqo感谢您的解释。

参数重载

我的意思是AddRange ,但我想我不会对此有任何吸引力。

@jcdickinson AddRange在原始提案中,但不在当前版本中。 它被 API 审查拒绝(参见 @terrajobst 的 https://github.com/dotnet/corefx/issues/14354#issuecomment-308190321):

我们应该删除所有AddRange方法,因为场景不清楚。 数组不太可能经常出现。 一旦涉及到更大的数组,问题是计算是否应该被缓存。 看到调用方的 for 循环清楚地表明您需要考虑这一点。

@gimpf我继续使用 xxHash32 对提案进行了polyfill 。 随意获取该实现。 它对实际的 xxHash32 向量进行了测试。

编辑

关于接口。 我完全意识到我正在从小山丘上造一座山 - 可以随意忽略。 我正在使用当前的提案来反对真实的东西,这是很多烦人的重复。

我一直在玩这个界面,现在明白为什么流畅的界面被拒绝了; 这是显著慢。

BenchmarkDotNet=v0.10.9, OS=Windows 10 Redstone 2 (10.0.15063)
Processor=Intel Core i7-4800MQ CPU 2.70GHz (Haswell), ProcessorCount=8
Frequency=2630626 Hz, Resolution=380.1377 ns, Timer=TSC
.NET Core SDK=2.0.2
  [Host]     : .NET Core 2.0.0 (Framework 4.6.00001.0), 64bit RyuJIT
  DefaultJob : .NET Core 2.0.0 (Framework 4.6.00001.0), 64bit RyuJIT

使用非内联方法作为哈希码源; 50 次调用 Add 与流畅的扩展方法:

| 方法 | 意思 | 错误 | 标准偏差 | 缩放 |
|------- |---------:|---------:|---------:|-------: |
| 添加 | 401.6 纳秒 | 1.262 纳秒 | 1.180 纳秒 | 1.00 |
| 理货 | 747.8 纳秒 | 2.329 纳秒 | 2.178 纳秒 | 1.86 |

但是,以下模式确实有效:

```c#
公共结构哈希代码:System.Collections.IEnumerable
{
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete("此方法是为集合初始化语法提供的。", error: true)]
public IEnumerator GetEnumerator() => throw new NotImplementedException();
}

public override int GetHashCode() => new HashCode()
{
    Age, // int
    { Name, StringComparer.Ordinal }, // use Comparer
    Hat // some arbitrary object
}.ToHashCode();

``

它还具有与当前提案相同的性能特征:

| 方法 | 意思 | 错误 | 标准偏差 | 缩放 |
|------------ |---------:|---------:|---------:|--- ----:|
| 添加 | 405.0 纳秒 | 2.130 纳秒 | 1.889 纳秒 | 1.00 |
| 初始化程序 | 400.8 纳秒 | 4.821 纳秒 | 4.274 纳秒 | 0.99 |

可悲的是,它有点像 hack,因为必须实现IEnumerable才能让编译器满意。 话虽如此, Obsolete甚至会在foreach上出错 - 您必须真的想要破坏事物才能遇到异常。 两者的 MSIL 基本相同。

@jcdickinson感谢您抓住这个问题。 我向您发送了合作者邀请,当您接受时告诉我,我将能够将此问题分配给您(同时分配给我自己)。

专业提示:一旦您接受,GitHub 将自动为您注册来自 repo 的所有通知(每天 500 多个),我建议将其更改为“Not Watching”,这将向您发送所有提及和问题通知你订阅了。

@jcdickinson ,我绝对对避免烦人重复的方法感兴趣(尽管我不知道人们对初始化语法的

  1. 你注意到的性能问题
  2. 流畅方法的返回值是结构的副本。 不小心最终丢失输入太容易了,例如:
var hc = new HashCode();
var newHc = hc.Add(foo);
hc.Add(bar);
return newHc.ToHashCode();

由于此线程上的提案已经获得批准(并且您正在顺利将其合并),因此我建议为任何更改启动一个新的 API 提案。

@karelz我相信@gimpf已经事先抓住了这个问题。 由于他对实现更熟悉,请将此问题分配给@gimpf 。 (编辑: nvm)

@terrajobst 对此的一种最后一刻的 API 请求。 由于我们将GetHashCode标记HashCode不是要比较的值,尽管它们是通常不可变/可比较的结构。 在这种情况下,我们是否也应该将Equals标记

[Obsolete("HashCode is a mutable struct and should not be compared with other HashCodes.", error: true)]
[EditorBrowsable(Never)]
// If this is too harsh, base.Equals() is fine as long as the [Obsolete] stays
public override bool Equals(object obj) => throw new NotSupportedException("HashCode is a mutable struct and should not be compared with other HashCodes.");

我认为Span做了类似的事情。

如果这被接受,那么我认为......

  1. 我会考虑在过时消息中使用should notmay not而不是cannot
  2. 如果异常仍然存在,我会在其消息中放置相同的字符串,以防万一该方法通过强制转换或开放泛型被调用。

@Joe4evr对我很好; 我已经更新了评论。 在GetHashCode异常中包含相同的消息也可能是有益的,然后:

public override int GetHashCode() => throw new NotSupportedException("HashCode is a mutable struct and should not be compared with other HashCodes.");

@morganbr你为什么重新打开这个?

在 CoreFX 中公开它的 PR 尚未通过。

@gimpf您是否有可用的基准代码和/或您是否能够快速了解​​ SpookilySharp nuget 软件包的性能。 在停滞了几年之后,我正在寻找该项目的灰尘,我很想知道它是如何站起来的。

@JonHanna他把它贴在这里: https :

@JonHanna ,我很想听听您的测试进展如何,这样我们就可以开始考虑在通用非加密散列 API 中什么有用。

@morganbr哪里是讨论此类 API 的合适论坛? 我希望这样的 API 不仅仅包含最低的公分母,也许一个好的 API 还需要改进的 JIT wrt 处理更大的结构。 在一个单独的问题中讨论所有可能更好地完成的事情......

@gimpf为您打开了一个。 dotnet/corefx#25666

@morganbr - 我们可以获取包含此提交的包名称和版本号吗?

@karelz ,您能否帮助@smitpatel提供软件包/版本信息?

我会尝试每天构建 .NET Core - 我会等到明天。
我不认为有一个包可以简单地依赖。

在这里向参与者提问。 Roslyn IDE 允许用户根据他们的 class/struct 中的一组字段/属性生成 GetHashCode impl。 理想情况下,人们可以使用在https://github.com/dotnet/corefx/pull/25013中添加的新 HashCode.Combine 。 但是,某些用户将无法访问该代码。 因此,我们希望仍然能够生成对他们有用的 GetHashCode。

最近,我们注意到我们生成的表单有问题。 也就是说,因为 VB 编译时默认启用溢出检查,我们的 impl 会导致溢出。 此外,VB 无法禁用代码区域的溢出检查。 对于整个装配体,它要么完全打开,要么完全关闭。

正因为如此,我希望能够用一个不会遇到这些问题的表单来替换我们提供的 impl。 理想情况下,生成的表单将具有以下属性:

  1. 每个使用的字段/属性在 GetHashCode 中有一行/两行。
  2. 没有溢出。
  3. 相当好的散列。 我们不期待惊人的结果。 但是,希望已经经过审查的东西是体面的,并且不会出现a + b + c + da ^ b ^ c ^ d通常遇到的问题。
  4. 对代码没有额外的依赖/要求。

例如,VB 的一种选择是生成如下内容:

return (a, b, c, d).GetHashCode()

但这取决于对 System.ValueTuple 的引用。 理想情况下,我们可以有一个即使在没有它的情况下也能工作的实现。

有谁知道可以处理这些约束的体面的散列算法? 谢谢!

——

注意:我们现有的发出代码是:

        Dim hashCode = -252780983
        hashCode = hashCode * -1521134295 + i.GetHashCode()
        hashCode = hashCode * -1521134295 + j.GetHashCode()
        Return hashCode

这显然可以溢出。

这对于 C# 来说也不是问题,因为我们可以在该代码周围添加unchecked { } 。 这种细粒度的控制在 VB 中是不可能的。

有谁知道可以处理这些约束的体面的散列算法? 谢谢!

好吧,你可以做Tuple.Create(...).GetHashCode() 。 显然这会导致分配,但似乎比抛出异常更好。

有什么理由不能告诉用户安装System.ValueTuple吗? 由于它是一个内置语言功能,我确定 System.ValueTuple 包基本上与所有平台都非常兼容,对吗?

显然这会导致分配,但似乎比抛出异常更好。

是的。 最好不要让它引起分配。

有什么理由不能告诉用户安装 System.ValueTuple 吗?

如果我们生成 ValueTuple 方法,这将是行为。 然而,再一次,如果我们能生成一些适合用户当前构建代码的方式的好东西,而不是让他们以重量级的方式改变他们的结构,那就太好了。

看起来 VB 用户确实应该有办法以合理的方式解决这个问题 :) 但是这样的方法让我望而却步 :)

@CyrusNajmabadi ,如果您确实需要在用户的代码中进行自己的哈希计算,CRC32 可能会起作用,因为它是表查找和 XOR 的组合(但不是可能溢出的算术)。 但也有一些缺点:

  1. CRC32 没有很大的熵(但它可能仍然比 Roslyn 现在发出的更好)。
  2. 您需要在代码中的某处放置一个 256 个条目的查找表或发出代码来生成查找表。

如果您还没有这样做,我希望您可以检测 HashCode 类型并在可能的情况下使用它,因为 XXHash 应该会好得多。

@morganbrhttps://github.com/dotnet/roslyn/pull/24161

我们执行以下操作:

  1. 如果可用,请使用 System.HashCode。 完毕。
  2. 否则,如果在 C# 中:
    2a. 如果不在检查模式下:生成展开的散列。
    2b. 如果处于选中模式:生成展开的哈希,包裹在“unchecked{}”中。
  3. 否则,如果在 VB 中:
    3b. 如果不在检查模式下:生成展开的哈希。
    3c。 如果处于选中模式,但可以访问 System.ValueTuple:生成Return (a, b, c, ...).GetHashCode()
    3d。 如果处于检查模式而无法访问 System.ValueTuple。 生成展开的hash,但是在VB中添加了一条很可能溢出的注释。

真是不幸的是'3d'。 基本上,使用 VB 但不使用 ValueTuple 或最近的系统的人将无法使用我们为他们生成合理的哈希算法。

您需要在代码中的某处放置一个 256 条目查找表

这将是完全不好吃的:)

表格生成代码也难吃吗? 至少以维基百科的例子来看,代码并不多(但它仍然必须在用户源代码中的某个地方)。

当无法通过任何引用的程序集使用编译器属性类定义时,像 Roslyn 那样(使用 IL)将 HashCode 源添加到项目中会有多糟糕?

当它们不能通过任何引用的程序集使用时,像 Roslyn 那样将 HashCode 源添加到项目中(更简单的)编译器属性类定义会有多糟糕?

  1. HashCode 源不需要溢出行为吗?
  2. 我浏览了 HashCode 源代码。 它的平凡。 将所有这些 goop 生成到用户的项目中将是非常重量级的。

我只是感到惊讶,根本没有让溢出数学在 VB 中工作的好方法:(

因此,至少,即使我们将两个值散列在一起,似乎我们也必须创建:

```c#
var hc1 = (uint)(value1?.GetHashCode() ?? 0); // 可以溢出
var hc2 = (uint)(value2?.GetHashCode() ?? 0); // 可以溢出

        uint hash = MixEmptyState();
        hash += 8; // can overflow

        hash = QueueRound(hash, hc1);
        hash = QueueRound(hash, hc2);

        hash = MixFinal(hash);
        return (int)hash; // can overflow
Note that this code already has 4 lines that can overflow.  It also has two helper functions you need to call (i'm ignoring MixEmptyState as that seems more like a constant).  MixFinal can *definitely* overflow:

```c#
        private static uint MixFinal(uint hash)
        {
            hash ^= hash >> 15;
            hash *= Prime2;
            hash ^= hash >> 13;
            hash *= Prime3;
            hash ^= hash >> 16;
            return hash;
        }

和 QueueRound 一样:

c# private static uint QueueRound(uint hash, uint queuedValue) { hash += queuedValue * Prime3; return Rol(hash, 17) * Prime4; }

所以我不诚实地看到这将如何工作:(

像 Roslyn 那样(使用 IL)将 HashCode 源添加到项目中会有多糟糕

你如何设想这个工作? 客户会写什么,然后编译器会做些什么来回应?

此外,如果 .Net 已经在表面 API 上公开了从 uint 转换为 int32(反之亦然)而不会溢出的公共助手,则可以解决所有这些问题。

那些存在吗? 如果是这样,我可以轻松编写 VB 版本,只需将它们用于需要在类型之间切换而不会溢出的情况。

表格生成代码也难吃吗?

我会这么认为。 我的意思是,从客户的角度考虑这一点。 他们只想要一个体面的 GetHashCode 方法,它很好地自包含并给出合理的结果。 拥有该功能并使用辅助废话来膨胀他们的代码将是非常令人不快的。 考虑到 C# 体验会很好,这也很糟糕。

通过在有符号和无符号 64 位类型的某种组合之间进行转换,您可能能够大致获得正确的溢出行为。 像这样的东西(未经测试,我不知道 VB 转换语法):

Dim hashCode = -252780983
hashCode = (Int32)((Int32)((Unt64)hashCode * -1521134295) + (UInt64)i.GetHashCode())

你怎么知道以下内容不会溢出?

c# (Int32)((Unt64)hashCode * -1521134295)

还是最终的 (int32) 演员表?

我没有意识到它会使用溢出检查的转换操作。 我想你可以在投射之前将它屏蔽到 32 位:

(Int32)(((Unt64)hashCode * -1521134295) & 0xFFFFFFFF)

大概是 31 位,作为 uint32.Max 的值也会在转换为 Int32 时溢出:)

那绝对有可能。 丑陋......但可能:) 这段代码中有很多演员。

好的。 我想我有一个可行的解决方案。 我们今天生成的算法的核心是:

c# hashCode = hashCode * -1521134295 + j.GetHashCode();

假设我们正在进行 64 位数学运算,但“hashCode”已被限制为 32 位。 那么<largest_32_bit> * -1521134295 + <largest_32_bit>不会溢出 64 位。 所以我们总是可以用 64 位进行数学运算,然后压缩到 32(或 32 位)以确保下一轮不会溢出。

谢谢!

@MaStr11 @morganbr @sharwell和这里的每个人。 我已经更新了我的代码来为 VB 生成以下内容:

        Dim hashCode As Long = 2118541809
        hashCode = (hashCode * -1521134295 + a.GetHashCode()) And Integer.MaxValue
        hashCode = (hashCode * -1521134295 + b.GetHashCode()) And Integer.MaxValue
        Return CType(hashCode And Integer.MaxValue, Integer)

有人可以检查我以确保这是有道理的,并且即使打开检查模式也不应该溢出吗?

@CyrusNajmabadi ,这不会溢出(因为 Int64.Max = Int32.Max*Int32.Max 和您的常量比这小得多)但是您将高位屏蔽为零,因此它只是一个 31 位哈希。 保留高位是否被视为溢出?

@CyrusNajmabadi hashCode是一个Long ,可以是从 0 到Integer.MaxValue任何地方。 为什么我会得到这个?

image

但不,它实际上不能溢出。

顺便说一句-我宁愿让 Roslyn 添加一个 NuGet 包而不是添加一个次优的哈希。

但是您将高位屏蔽为零,因此它只是一个 31 位散列。 保留高位是否被视为溢出?

那是个很好的观点。 我想我正在考虑使用 uint 的另一种算法。 因此,为了安全地从 long 转换为 uint,我需要不包含符号位。 然而,由于这都是有符号数学,我认为只屏蔽 0xffffffff 确保我们在添加每个条目后只保留底部 32 位就可以了。

我宁愿让 Roslyn 添加一个 NuGet 包而不是添加一个次优哈希。

如果用户愿意,他们已经可以做到这一点。 这是关于当用户不或不能添加这些依赖项时该怎么做。 这也是关于为用户提供合理的“足够好”的哈希。 即比人们经常采用的常见“x + y + z”方法更好的东西。 它并不打算成为“最佳”,因为对于所有用户进行散列时,“最佳”是什么并没有很好的定义。 请注意,我们在这里采用的方法是编译器已经为匿名类型发出的方法。 它表现出相当好的行为,同时不会给用户的代码增加大量的复杂性。 随着时间的推移,随着越来越多的用户能够向前发展,对于大多数人来说,这种可以慢慢消失并被 HashCode.Combine 取代。

所以我做了一些工作,并提出了以下我认为可以解决所有问题的方法:

        Dim hashCode As Long = 2118541809
        hashCode = (hashCode * -1521134295 + a.GetHashCode()).GetHashCode()
        hashCode = (hashCode * -1521134295 + b.GetHashCode()).GetHashCode()
        Return CType(hashCode, Integer)

有趣的部分是在(hashCode * -1521134295 + a.GetHashCode())生成的 int64 值上专门调用.GetHashCode() (hashCode * -1521134295 + a.GetHashCode()) 。 在这个 64 位值上调用 .GetHashCode 有两个很好的特性可以满足我们的需求。 首先,它确保 hashCode 只在其中存储合法的 int32 值(这使得最终返回的强制转换始终可以安全执行)。 其次,它确保我们不会丢失我们正在使用的 int64 临时值的高 32 位中的任何有价值的信息。

@CyrusNajmabadi实际上提供安装软件包是我要问的。 让我不必这样做。

如果您键入 HashCode,则如果 System.HashCode 在 MS nuget 包中提供,则 Roslyn 将提供它。

我希望它生成不存在的 GetHashCode 重载并在同一操作中安装包。

我认为这对大多数用户来说不是一个合适的选择。 添加依赖项是一项非常重量级的操作,不应强迫用户进行。 用户可以决定做出这些选择的正确时间,IDE 会尊重它。 到目前为止,这就是我们对所有功能采取的方法,而且人们似乎喜欢这种方法。

注意:这个 api 甚至包含在哪个 nuget 包中供我们添加引用?

该实现位于 System.Private.CoreLib.dll 中,因此它将作为运行时包的一部分出现。 合约是 System.Runtime.dll。

好的。 如果是这种情况,那么听起来用户如果/当他们转移到更新的目标框架时会得到这个。 那种事情根本不是我对用户项目执行“生成等于+哈希码”的步骤。

此页面是否有帮助?
0 / 5 - 0 等级

相关问题

chunseoklee picture chunseoklee  ·  3评论

bencz picture bencz  ·  3评论

Timovzl picture Timovzl  ·  3评论

sahithreddyk picture sahithreddyk  ·  3评论

jchannon picture jchannon  ·  3评论