是字符串.GetHashCode考虑整个字符串或只考虑它的一部分
本文关键字:字符串 一部分 GetHashCode | 更新日期: 2023-09-27 18:01:46
我只是好奇,因为我猜它会对性能产生影响。它会考虑整个字符串吗?如果是,它将是缓慢的长串。如果它只考虑字符串的一部分,它的性能会很差(例如,如果它只考虑字符串的开头,如果一个HashSet
包含的大部分字符串都是相同的
当您遇到这样的问题时,请务必获取参考源代码。它比您从反编译器中看到的要多得多。选择一个与您首选的。net目标相匹配的方法,该方法在不同版本之间发生了很大的变化。我将在这里复制它的。net 4.5版本,从源代码中检索。净4.5 ' 4.6.0.0 ' ' 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源代码中关闭特性。WIN32是在目标是32位版本的框架时定义的,64位版本的mscorlib.dll是单独构建的,并存储在GAC的不同子目录中。
- s_UseRandomizedStringHashing变量启用了哈希算法的安全版本,旨在使程序员避免做一些不明智的事情,例如使用GetHashCode()为密码或加密之类的东西生成哈希。它是通过app.exe.config文件 中的一个条目启用的。固定的语句使索引字符串便宜,避免了常规索引器所做的边界检查。
- 第一个Assert确保字符串应该是零终止的,这是允许循环中的优化所必需的
- 第二个Assert确保字符串与一个地址对齐,该地址应该是4的倍数,这是保持循环性能所必需的
- 手动展开循环,对于32位版本,每个循环消耗4个字符。强制转换为int*是在int(32位)中存储2个字符(2 x 16位)的技巧。循环后的额外语句处理长度不是4的倍数的字符串。请注意,散列中可能包含零终止符,也可能不包含零终止符,如果长度是偶数则不会包含零终止符。它查看字符串中的所有字符,回答您的问题
- 64位版本的循环是不同的,由2手动展开。请注意,它在嵌入的零处提前终止,因此不会查看所有字符。除此之外非常罕见。这很奇怪,我只能猜测这与字符串可能非常大有关。但是想不出一个实际的例子
- 最后的调试代码确保框架中的任何代码都不会依赖于在运行之间可重复的哈希码。
- 哈希算法非常标准。值1566083941是一个幻数,一个在梅森旋风中常见的素数。
检查源代码(由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;
}