朋友吐槽我为什么这么傻不在源生成器中用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  

MethodCountMeanErrorStdDevRatioRatioSDGen0AllocatedAlloc Ratio
SlowNonRandomizedHash00.6516 ns1.9625 ns0.1076 ns0.550.08--NA
NonRandomizedHash01.0323 ns0.3411 ns0.0187 ns0.880.02--NA
GetHashCode01.1765 ns0.4219 ns0.0231 ns1.000.02--NA
XxHash32Span02.9876 ns0.1505 ns0.0083 ns2.540.04--NA
XxHash32GetBytes09.8159 ns0.5013 ns0.0275 ns8.350.14--NA
GetHashCode11.3779 ns0.2813 ns0.0154 ns1.000.01--NA
XxHash32Span13.9676 ns0.1327 ns0.0073 ns2.880.03--NA
SlowNonRandomizedHash112.4232 ns2.1749 ns0.1192 ns9.020.11--NA
NonRandomizedHash115.7222 ns0.4655 ns0.0255 ns11.410.11--NA
XxHash32GetBytes116.0994 ns9.8530 ns0.5401 ns11.690.360.003832 BNA
GetHashCode105.2859 ns0.3512 ns0.0193 ns1.000.00--NA
XxHash32Span105.7876 ns1.0571 ns0.0579 ns1.090.01--NA
NonRandomizedHash1024.3032 ns0.5894 ns0.0323 ns4.600.02--NA
XxHash32GetBytes1026.1983 ns2.5796 ns0.1414 ns4.960.030.004840 BNA
SlowNonRandomizedHash1049.6894 ns1.5164 ns0.0831 ns9.400.03--NA
XxHash32Span10027.7102 ns2.6360 ns0.1445 ns0.500.00--NA
GetHashCode10055.2268 ns5.2640 ns0.2885 ns1.000.01--NA
XxHash32GetBytes100159.1823 ns25.1064 ns1.3762 ns2.880.030.0248208 BNA
NonRandomizedHash100168.0799 ns0.7943 ns0.0435 ns3.040.01--NA
SlowNonRandomizedHash100601.4293 ns160.7347 ns8.8104 ns10.890.15--NA

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

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

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

原创作者: fs7744 转载于: https://www.cnblogs.com/fs7744/p/18352853
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值