簡體   English   中英

使 GetHashCode 方法對不同進程的字符串表現相同

[英]Make GetHashCode method behave the same for strings for different processes

如果我運行這個:

Console.WriteLine("Foo".GetHashCode());
Console.WriteLine("Foo".GetHashCode());

它會打印相同的數字兩次,但如果我再次運行該程序,它將打印不同的數字。

根據 Microsoft 和 inte.net 上的其他地方,我們不能依賴 GetHashCode function 返回相同的值。 但是,如果我計划僅在字符串上使用它,我該如何利用它並期望始終為相同的字符串返回相同的值? 我喜歡它的速度。 如果我能得到它的源代碼並在我的應用程序中使用它,那就太好了。

  • 我需要它的原因(你可以跳過這部分)

    我有很多復雜的對象,我需要序列化它們並在進程間通信之間發送它們。 如您所知,BinaryFormatter 現在已過時,因此我嘗試使用 System.Text.Json 來序列化我的對象。 那非常快,但是因為我有很多復雜的對象反序列化效果不佳,因為我大量使用多態性。 然后我嘗試了 Newtonsoft (json.net),它非常適合這個例子: https://stackoverflow.com/a/71398251/637142 但是速度很慢。 然后我決定我將使用最好的選擇,那就是 ProtoBuffers。 所以我使用的是 protobuf.net,效果很好,但問題是我有一些非常復雜的對象,放置數千個屬性很痛苦。 例如,我有一個基數 class,它被其他 70 個類使用,我不得不為每個類放置一個 inheritance 的屬性,這是不切實際的。 所以最后我決定實現我自己的算法,它並不那么復雜。 我只需要遍歷每個 object 的屬性,如果一個屬性不是值類型,則遞歸地再次遍歷它們。 但是為了使我構建的自定義序列化速度更快,我需要將所有反射對象存儲在 memory 中。所以我有一個包含類型和 propertyInfos 的字典。 所以我第一次序列化它會很慢,但后來它甚至比 ProtoBuf 更快。 所以是的,這種方法很快,但每個進程都必須具有完全相同的 object,否則它將無法工作。 另一個權衡是它的大小比 protobuf 大,因為每次我序列化一個屬性時,我都會在之前包含該屬性的全名。 因此,我想將屬性的全名 hash 轉換為 integer(4 個字節) ,而 GetHashCode() function 正是這樣做的!

很多人可能會建議我應該使用 MD5 或其他替代方法,但請看一下性能差異:

// generate 1 million random GUIDS
List<string> randomGuids = new List<string>();
for (int i = 0; i < 1_000_000; i++)
    randomGuids.Add(Guid.NewGuid().ToString());

// needed to measure time
var sw = new Stopwatch();
sw.Start();


// using md5 (takes aprox 260 ms)
using (var md5 = MD5.Create())
{
    sw.Restart();
    foreach (var guid in randomGuids)
    {
        byte[] inputBytes = System.Text.Encoding.ASCII.GetBytes(guid);
        byte[] hashBytes = md5.ComputeHash(inputBytes);
        // make use of hashBytes to make sure code is compiled
        if (hashBytes.Length == 44)
            throw new Exception();
    }
    var elapsed = sw.Elapsed.TotalMilliseconds;
    Console.WriteLine($"md5: {elapsed}");
}

// using .net framework 4.7 source code (takes aprox 65 ms)
{
    [System.Security.SecuritySafeCritical]  // auto-generated
    [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
    static int GetHashCodeDotNetFramework4_7(string str)
    {

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

        unsafe
        {
            fixed (char* src = str)
            {

#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 ^= -484733382;
#endif
                return hash1 + (hash2 * 1566083941);
            }
        }
    }

    sw.Restart();
    foreach (var guid in randomGuids)
        if (GetHashCodeDotNetFramework4_7(guid) == 1234567)
            throw new Exception("this will probably never happen");

    var elapsed = sw.Elapsed.TotalMilliseconds;
    Console.WriteLine($".NetFramework4.7SourceCode: {elapsed}");
}

// using .net 6 built in GetHashCode function (takes aprox: 22 ms)
{
    sw.Restart();
    foreach (var guid in randomGuids)
        if (guid.GetHashCode() == 1234567)
            throw new Exception("this will probably never happen");

    var elapsed = sw.Elapsed.TotalMilliseconds;
    Console.WriteLine($".net6: {elapsed}");
}

在發布模式下運行這些我的結果:

md5: 254.7139
.NetFramework4.7SourceCode: 74.2588
.net6: 23.274

我從這個鏈接獲得了 .NET Framework 4.8 的源代碼: https://referencesource.microsoft.com/#mscorlib/system/string.cs,8281103e6f23cb5c

無論如何在 inte.net 上搜索我發現這篇有用的文章: https://andrewlock.net/why-is-string-gethashcode-different-each-time-i-run-my-program-in.net-core/

我已經完全按照它告訴你的去做了,我還添加了:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <runtime>
        <UseRandomizedStringHashAlgorithm enabled="1" />
    </runtime>
</configuration>


到我的 app.config 文件,但每次運行我的應用程序時, "foo".GetHashCode()仍然會得到不同的值。

如何使GetHashcode()方法始終為 .net 6 中的字符串“foo”返回相同的值?


編輯

我將只使用 .net 框架 4.8 源代碼的解決方案,執行時間為 73 毫秒並繼續。 我只是很好奇,想了解為什么哈希碼的構建速度如此之快。

至少我現在明白為什么 hash 每次都不一樣了。 通過查看.net 6的源碼,之所以每次都是不同的hash,是因為這個:


namespace System
{
    internal static partial class Marvin
    {

        ... .net source code
        ....

        public static ulong DefaultSeed { get; } = GenerateSeed();

        private static unsafe ulong GenerateSeed()
        {
            ulong seed;
            Interop.GetRandomBytes((byte*)&seed, sizeof(ulong));
            return seed;
        }
    }
}

結果,我只是為了好玩而嘗試了這個,但仍然沒有用:

    var ass = typeof(string).Assembly;
    var marvin = ass.GetType("System.Marvin");
    var defaultSeed = marvin.GetProperty("DefaultSeed");
    var value = defaultSeed.GetValue(null); // returns 3644491462759144438

    var field = marvin.GetField("<DefaultSeed>k__BackingField", BindingFlags.NonPublic | BindingFlags.Static);
    ulong v = 3644491462759144438;
    field.SetValue(null, v);

但在最后一行我得到異常: System.FieldAccessException: 'Cannot set initonly static field '<DefaultSeed>k__BackingField' after type 'System.Marvin' is initialized.'

但即使這有效,也將是非常不安全的。 我希望某些東西的執行速度慢 3 倍,然后繼續前進。

為什么不使用您分享的文章中建議的實現?

我正在復制它以供參考:

static int GetDeterministicHashCode(this string str)
{
    unchecked
    {
        int hash1 = (5381 << 16) + 5381;
        int hash2 = hash1;

        for (int i = 0; i < str.Length; i += 2)
        {
            hash1 = ((hash1 << 5) + hash1) ^ str[i];
            if (i == str.Length - 1)
                break;
            hash2 = ((hash2 << 5) + hash2) ^ str[i + 1];
        }

        return hash1 + (hash2 * 1566083941);
    }
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM