朋友吐槽我为什么这么傻不在源生成器中用string.GetHashCode, 而要用一个不够优化的hash方法

明明有更好的hash方法

(ps: 添加 XxHash32 测试, XxHash32 大小写敏感)

有位朋友对我吐槽前几天我列举的在源生成器的生成db映射实体的优化点 提前生成部分 hashcode 进行比较

所示代码

public static void GenerateReadTokens(this IDataReader reader, Span<int> s)
{
    for (int i = 0; i < reader.FieldCount; i++)
    {
        var name = reader.GetName(i);
        var type = reader.GetFieldType(i);
        switch (EntitiesGenerator.SlowNonRandomizedHash(name))
        {
            
            case 742476188U:
                s[i] = type == typeof(int) ? 1 : 2; 
                break;

            case 2369371622U:
                s[i] = type == typeof(string) ? 3 : 4; 
                break;

            case 1352703673U:
                s[i] = type == typeof(float) ? 5 : 6; 
                break;

            default:
                break;
        }
    }
}

这里为什么不用 string.GetHashCode, 而要用 SlowNonRandomizedHash(name), 有更好的方法不用,真是傻

当时俺也只能 囧 着脸给ta解释 string.GetHashCode真的没办法用,

可惜口头几句解释再多,一时也无法摆脱ta鄙视的目光

只有在此多写几句“狡辩”

“狡辩”

首先其实NormalizedHash 性能很强的,其实现如下

public static uint SlowNonRandomizedHash(this string? value)
{
    uint hash = 0;
    if (!string.IsNullOrEmpty(value))
    {
        hash = 2166136261u;
        foreach (char c in value!)
        {
            hash = (char.ToLowerInvariant(c) ^ hash) * 16777619;
        }
    }
    return hash;
}

但是不管性能强不强,也不是只能用这个方法的原因

其实真实原因很多人都知道,都是大家的默认常识了:net code string.GetHashCode是随机的,多次运行程序,同一个字符串可能会在每次运行都有不同的哈希值

比如 18年的文章 Why is string.GetHashCode() different each time I run my program in .NET Core?

这里简单复述一下原文内容

以这个非常简单的程序为例,它连续两次调用一个字符串GetHashCode()

using System;

static class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Hello World!".GetHashCode());
        Console.WriteLine("Hello World!".GetHashCode());
    }
}

如果在 .NET Framework 上运行此程序,则每次运行该程序时,都会获得相同的值:

> dotnet run -c Release -f net471
-1989043627
-1989043627

> dotnet run -c Release -f net471
-1989043627
-1989043627

> dotnet run -c Release -f net471
-1989043627
-1989043627

相反,如果为 .NET Core 编译同一程序,则在同一程序执行中每次调用都会获得相同的值,但对于不同的程序执行,将获得不同的值:GetHashCode()

> dotnet run -c Release -f netcoreapp2.1
-1105880285
-1105880285

> dotnet run -c Release -f netcoreapp2.1
1569543669
1569543669

> dotnet run -c Release -f netcoreapp2.1
-1477343390
-1477343390

努力查找之后,在微软官方文档给出过使用GetHashCode()方法的建议。其明确提示,不应将GetHashCode()方法产生的hash值当作为相同能持久化的值使用。

The hash code itself is not guaranteed to be stable. Hash codes for identical strings can differ across .NET implementations, across .NET versions, and across .NET platforms (such as 32-bit and 64-bit) for a single version of .NET. In some cases, they can even differ by application domain. This implies that two subsequent runs of the same program may return different hash codes.

为什么要用随机化的 hash?

Stephen Toub 在一个issue 中提到了这个问题的答案:

Q: Why .NET Core utilize randomized string hashing?
问:为什么 .NET Core 使用随机字符串哈希?
A: Security, prevention against DoS attacks, etc.
A:安全性、防止 DoS 攻击等。

原文很详细的解释有关安全的内容,这里就不作详细复述了

那么有没有更好的 hash 方法呢?

当然肯定是有的,string 类内部其实就有,

感兴趣的童鞋可以阅读源码 https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/String.Comparison.cs#L923

里面 大小写敏感和不敏感都有实现, 其代码比上面18年文章列举的方法还有更多性能优化

不过内部方法,我们没有办法可以直接使用

但是呢? 我们有黑魔法可以直接使用

public static partial class StringHashing
{
    [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "GetNonRandomizedHashCodeOrdinalIgnoreCase")]
    public static extern int Hash(this string c);
}

比较一下

我们都写到这里了,不比一下性能,大家肯定不服气

来一段简单的比较

[ShortRunJob, MemoryDiagnoser, Orderer(summaryOrderPolicy: SummaryOrderPolicy.FastestToSlowest), GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory), CategoriesColumn]
public class StringHashingBenchmarks
{
    [Params(0, 1, 10, 100)]
    public int Count { get; set; }

    public string Str { get; set; }

    [GlobalSetup]
    public void Setup()
    {
        var s = string.Join("", Enumerable.Repeat("_", Count));
        var b = Encoding.UTF8.GetBytes(s);
        Random.Shared.NextBytes(b);
        Str = Encoding.UTF8.GetString(b);
    }

    [Benchmark(Baseline = true)]
    public int GetHashCode()
    {
        return Str.GetHashCode();
    }

    [Benchmark]
    public uint SlowNonRandomizedHash()
    {
        return Str.SlowNonRandomizedHash();
    }

    [Benchmark]
    public int NonRandomizedHash()
    {
        return Str.Hash();
    }

    [Benchmark]
    public uint XxHash32GetBytes()
    {
        return XxHash32.HashToUInt32(Encoding.UTF8.GetBytes(Str));
    }

    [Benchmark]
    public uint XxHash32Span()
    {
        return XxHash32.HashToUInt32(MemoryMarshal.AsBytes(Str.AsSpan()));
    }
}

结果


BenchmarkDotNet v0.14.0, Windows 10 (10.0.19045.4651/22H2/2022Update)
Intel Core i7-10700 CPU 2.90GHz, 1 CPU, 16 logical and 8 physical cores
.NET SDK 9.0.100-preview.5.24307.3
  [Host]   : .NET 8.0.6 (8.0.624.26715), X64 RyuJIT AVX2
  ShortRun : .NET 8.0.6 (8.0.624.26715), X64 RyuJIT AVX2

Job=ShortRun  IterationCount=3  LaunchCount=1  
WarmupCount=3  

Method Count Mean Error StdDev Ratio RatioSD Gen0 Allocated Alloc Ratio
SlowNonRandomizedHash 0 0.6516 ns 1.9625 ns 0.1076 ns 0.55 0.08 - - NA
NonRandomizedHash 0 1.0323 ns 0.3411 ns 0.0187 ns 0.88 0.02 - - NA
GetHashCode 0 1.1765 ns 0.4219 ns 0.0231 ns 1.00 0.02 - - NA
XxHash32Span 0 2.9876 ns 0.1505 ns 0.0083 ns 2.54 0.04 - - NA
XxHash32GetBytes 0 9.8159 ns 0.5013 ns 0.0275 ns 8.35 0.14 - - NA
GetHashCode 1 1.3779 ns 0.2813 ns 0.0154 ns 1.00 0.01 - - NA
XxHash32Span 1 3.9676 ns 0.1327 ns 0.0073 ns 2.88 0.03 - - NA
SlowNonRandomizedHash 1 12.4232 ns 2.1749 ns 0.1192 ns 9.02 0.11 - - NA
NonRandomizedHash 1 15.7222 ns 0.4655 ns 0.0255 ns 11.41 0.11 - - NA
XxHash32GetBytes 1 16.0994 ns 9.8530 ns 0.5401 ns 11.69 0.36 0.0038 32 B NA
GetHashCode 10 5.2859 ns 0.3512 ns 0.0193 ns 1.00 0.00 - - NA
XxHash32Span 10 5.7876 ns 1.0571 ns 0.0579 ns 1.09 0.01 - - NA
NonRandomizedHash 10 24.3032 ns 0.5894 ns 0.0323 ns 4.60 0.02 - - NA
XxHash32GetBytes 10 26.1983 ns 2.5796 ns 0.1414 ns 4.96 0.03 0.0048 40 B NA
SlowNonRandomizedHash 10 49.6894 ns 1.5164 ns 0.0831 ns 9.40 0.03 - - NA
XxHash32Span 100 27.7102 ns 2.6360 ns 0.1445 ns 0.50 0.00 - - NA
GetHashCode 100 55.2268 ns 5.2640 ns 0.2885 ns 1.00 0.01 - - NA
XxHash32GetBytes 100 159.1823 ns 25.1064 ns 1.3762 ns 2.88 0.03 0.0248 208 B NA
NonRandomizedHash 100 168.0799 ns 0.7943 ns 0.0435 ns 3.04 0.01 - - NA
SlowNonRandomizedHash 100 601.4293 ns 160.7347 ns 8.8104 ns 10.89 0.15 - - NA

当然,我们比较的 hash code 是大小写敏感的, 而其他两个是大小写不敏感的,

但是其差距都非常小,所以可以说都是很强的方法了

可惜 UnsafeAccessor 这些黑魔法无法在源生成器中使用

posted @ 2024-08-10 22:06  victor.x.qu  阅读(390)  评论(2编辑  收藏  举报