C#字符串的GetHashCode()是如何实现的?

Posted

技术标签:

【中文标题】C#字符串的GetHashCode()是如何实现的?【英文标题】:How is GetHashCode() of C# string implemented? 【发布时间】:2013-03-02 12:29:04 【问题描述】:

我只是好奇,因为我猜它会对性能产生影响。它是否考虑完整的字符串?如果是,那么长字符串会很慢。如果它只考虑字符串的一部分,它的性能会很差(例如,如果它只考虑字符串的开头,如果 HashSet 包含大部分相同的字符串,它的性能就会很差。

【问题讨论】:

dotnetperls.com/gethashcode “它的性能会很差” - 与什么替代品相比很差?显然,在 HashSet 中存储非常非常长的字符串会比存储短字符串要慢,但多久会这样做一次? 在电脑上"".GetHashCode() == 371857150。每个人都一样吗? @ColonelPanic 这就是下面发布的代码的样子,假设每个人都在使用运行时的发布版本。 csharppad.com 为"".GetHashCode() 产生与您相同的值 【参考方案1】:

当您有此类问题时,请务必获取Reference Source source code。它比您从反编译器中看到的要多得多。选择与您首选的 .NET 目标匹配的那个,该方法在版本之间发生了很大变化。我将在这里复制它的 .NET 4.5 版本,从 Source.NET 4.5\4.6.0.0\net\clr\src\BCL\System\String.cs\604718\String.cs

        public override int GetHashCode()  

#if FEATURE_RANDOMIZED_STRING_HASHING
            if(HashHelpers.s_UseRandomizedStringHashing)
             
                return InternalMarvin32HashString(this, this.Length, 0);
             
#endif // FEATURE_RANDOMIZED_STRING_HASHING 

            unsafe  
                fixed (char *src = this) 
                    Contract.Assert(src[this.Length] == '\0', "src[this.Length] == '\\0'");
                    Contract.Assert( ((int)src)%4 == 0, "Managed string should start at 4 bytes boundary");

#if WIN32
                    int hash1 = (5381<<16) + 5381; 
#else 
                    int hash1 = 5381;
#endif 
                    int hash2 = hash1;

#if WIN32
                    // 32 bit machines. 
                    int* pint = (int *)src;
                    int len = this.Length; 
                    while (len > 2) 
                    
                        hash1 = ((hash1 << 5) + hash1 + (hash1 >> 27)) ^ pint[0]; 
                        hash2 = ((hash2 << 5) + hash2 + (hash2 >> 27)) ^ pint[1];
                        pint += 2;
                        len  -= 4;
                     

                    if (len > 0) 
                     
                        hash1 = ((hash1 << 5) + hash1 + (hash1 >> 27)) ^ pint[0];
                     
#else
                    int     c;
                    char *s = src;
                    while ((c = s[0]) != 0)  
                        hash1 = ((hash1 << 5) + hash1) ^ c;
                        c = s[1]; 
                        if (c == 0) 
                            break;
                        hash2 = ((hash2 << 5) + hash2) ^ c; 
                        s += 2;
                    
#endif
#if DEBUG 
                    // We want to ensure we can change our hash function daily.
                    // This is perfectly fine as long as you don't persist the 
                    // value from GetHashCode to disk or count on String A 
                    // hashing before string B.  Those are bugs in your code.
                    hash1 ^= ThisAssembly.DailyBuildNumber; 
#endif
                    return hash1 + (hash2 * 1566083941);
                
             
        

这可能比你想象的要多,我会稍微注释一下代码:

#if 条件编译指令使该代码适应不同的 .NET 目标。 FEATURE_XX 标识符在其他地方定义,并在整个 .NET 源代码中关闭功能。当目标是32位版本的框架时定义WIN32,64位版本的mscorlib.dll单独构建并存放在GAC的不同子目录中。 s_UseRandomizedStringHashing 变量启用了哈希算法的安全版本,旨在让程序员避免使用 GetHashCode() 为密码或加密等生成哈希值等不明智的事情。它由 app.exe.config 文件中的an entry 启用 fixed 语句保持索引字符串的成本低廉,避免了常规索引器进行的边界检查 第一个 Assert 确保字符串按照应有的方式以零结尾,允许在循环中进行优化 第二个 Assert 确保字符串与应为 4 的倍数的地址对齐,这是保持循环性能所必需的 循环是手动展开的,对于 32 位版本,每个循环消耗 4 个字符。转换为 int* 是将 2 个字符(2 x 16 位)存储在 int(32 位)中的技巧。循环之后的额外语句处理长度不是 4 的倍数的字符串。请注意,零终止符可能包含在散列中,也可能不包含,如果长度是偶数则不会。它查看所有字符串中的字符,回答你的问题 64 位版本的循环以不同的方式完成,手动展开 2。请注意,它在嵌入的零处提前终止,因此不会查看所有字符。否则非常罕见。这很奇怪,我只能猜测这与可能非常大的字符串有关。但是想不出一个实际的例子 最后的调试代码确保框架中的任何代码都不会依赖哈希码在运行之间可重现。 哈希算法非常标准。值 1566083941 是一个幻数,是Mersenne twister 中常见的质数。

【讨论】:

链接在帖子的第一句。 “它在嵌入的零上提前终止” - 这很奇怪。我对其进行了测试,果然 64 位版本会忽略 \0 之后的字符(32 位不会)。而且由于 NULL 是一个有效的 unicode 字符,这在技术上是一个错误 IMO。 @locster 该错误已通过“按设计”关闭。有没有人找到解释为什么 64 位版本在 \0 之后不散列字符? “旨在让程序员避免做一些不明智的事情,例如使用 GetHashCode() 为密码或加密等内容生成哈希”这对你没有多大帮助,有点但不是很多。如果您正在对直接来自用户输入的字符串进行散列处理,从而使您容易受到 hash-dos 攻击,这将有所帮助。 为什么不只计算一次,因为字符串是不可变的?【参考方案2】:

检查源代码(由ILSpy 提供),我们可以看到它确实迭代了字符串的长度。

// string
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail), SecuritySafeCritical]
public unsafe override int GetHashCode()

    IntPtr arg_0F_0;
    IntPtr expr_06 = arg_0F_0 = this;
    if (expr_06 != 0)
    
        arg_0F_0 = (IntPtr)((int)expr_06 + RuntimeHelpers.OffsetToStringData);
    
    char* ptr = arg_0F_0;
    int num = 352654597;
    int num2 = num;
    int* ptr2 = (int*)ptr;
    for (int i = this.Length; i > 0; i -= 4)
    
        num = ((num << 5) + num + (num >> 27) ^ *ptr2);
        if (i <= 2)
        
            break;
        
        num2 = ((num2 << 5) + num2 + (num2 >> 27) ^ ptr2[(IntPtr)4 / 4]);
        ptr2 += (IntPtr)8 / 4;
    
    return num + num2 * 1566083941;

【讨论】:

是的,我看到了,但我不知道它的作用:o 等等。在二读时,它似乎与我的 ILSpy 中的代码不同。我的没有长度的 for 循环。也许它在不同平台上的实现方式不同 嗯,它对字符串进行哈希处理。你确实说过你想知道它的作用,所以它就是这样。不同版本的 CLR 的字符串哈希算法不同。 @LouisRhys - 那是来自 .NET 2.0 的那个(因为我已经在 ILSpy 中加载了它)。我已将其替换为 .NET 4.0 中的那个 - 看起来非常相似。

以上是关于C#字符串的GetHashCode()是如何实现的?的主要内容,如果未能解决你的问题,请参考以下文章

在 C# 中实现 Equals 但不是 GetHashCode [重复]

dotnet C# 基础 为什么 GetHashCode 推荐只取只读属性或字段做哈希值

为啥我需要覆盖 C# 中的 .Equals 和 GetHashCode [重复]

在c#中散列一个数组

C# 存储数字的安全方式?

String.GetHashCode() 返回不同的值