朋友吐槽我為什麼這麼傻不在源生成器中用string.GetHashCode, 而要用一個不夠最佳化的hash方法

victor.x.qu發表於2024-08-10

明明有更好的hash方法

有位朋友對我吐槽前幾天我列舉的在源生成器的生成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();
    }
}

結果


BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3880/23H2/2023Update/SunValley3)
13th Gen Intel Core i9-13900KF, 1 CPU, 32 logical and 24 physical cores
.NET SDK 9.0.100-preview.6.24328.19
  [Host]   : .NET 8.0.7 (8.0.724.31311), X64 RyuJIT AVX2
  ShortRun : .NET 8.0.7 (8.0.724.31311), X64 RyuJIT AVX2

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

Method Count Mean Error StdDev Ratio RatioSD Allocated Alloc Ratio
SlowNonRandomizedHash 0 0.3286 ns 0.0727 ns 0.0040 ns 0.69 0.01 - NA
GetHashCode 0 0.4751 ns 0.1093 ns 0.0060 ns 1.00 0.00 - NA
NonRandomizedHash 0 0.6614 ns 0.0339 ns 0.0019 ns 1.39 0.02 - NA
GetHashCode 1 0.5686 ns 0.0881 ns 0.0048 ns 1.00 0.00 - NA
NonRandomizedHash 1 0.6559 ns 0.0254 ns 0.0014 ns 1.15 0.01 - NA
SlowNonRandomizedHash 1 7.3752 ns 0.2379 ns 0.0130 ns 12.97 0.11 - NA
GetHashCode 10 3.1627 ns 0.2081 ns 0.0114 ns 1.00 0.00 - NA
NonRandomizedHash 10 16.1921 ns 1.1773 ns 0.0645 ns 5.12 0.02 - NA
SlowNonRandomizedHash 10 44.4825 ns 2.8742 ns 0.1575 ns 14.06 0.01 - NA
GetHashCode 100 40.4233 ns 0.7217 ns 0.0396 ns 1.00 0.00 - NA
NonRandomizedHash 100 110.2494 ns 13.1581 ns 0.7212 ns 2.73 0.02 - NA
SlowNonRandomizedHash 100 362.0329 ns 11.0681 ns 0.6067 ns 8.96 0.02 - NA

當然,我們比較的 hash code 是大小寫敏感的, 而其他兩個是大小寫不敏感的,

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

可惜 UnsafeAccessor 這些黑魔法無法在源生成器中使用

相關文章