• [.NET / C#] String.GetHashCode()

    2022. 2. 26.

    by. xxx123

     

     

    이번엔 String.GetHashCode()가 어떻게 값을 반환하며 .NET 버전에 따라 어떤 차이를 갖는지에 대해 설명해 보려 한다.

     

     

     

     

    Type별 GetHashCode()

    우리는 종종 .NET에서 제공하는 GetHashCode가 특정 값에 대해 항상 같은 결과를 내 놓을 것이라고 생각한다.

    이 것은 반은 맞고 반은 틀린 내용이다.

    .NET Core 5.0을 기준으로 다음과 같이 테스트를 진행해 보았다.

    	static void Main(string[] args)
            {
                int num = 10;
                int hash1 = num.GetHashCode(); // hash1 : 10
                int hash2 = num.GetHashCode(); // hash2 : 10
            }

    위 결과 처럼 num이라는 int 변수의 HashCode는 언제나 10이다.

    이것은 몇 번을 수행하던, 프로그램을 껐다가 다시 실행하건 언제나 같은 결과를 반환한다.

     

    그렇다면 다음 코드는 어떤 결과가 나올 것인가?

            static void Main(string[] args)
            {
                string str = "Hello World";
                int hash1 = str.GetHashCode(); // hash1 : 604018020
                int hash2 = str.GetHashCode(); // hash2 : 604018020
            }

     

    마찬 가지로 hash1과 hash2는 동일한 결과를 나타낸다. 하지만 프로그램을 다시 실행시켜 보면 다음과 같은 결과가 나온다.

            static void Main(string[] args)
            {
                string str = "Hello World";
                int hash1 = str.GetHashCode(); // hash1 : -2124782429
                int hash2 = str.GetHashCode(); // hash2 : -2124782429
            }

    int의 HashCode값은 언제나 같은 값을 반환하는 반면에 String의 HashCode값은 동일한 도메인 내에서는 동일한 HashCode값을 반환하지만, 새롭게 시작한 다른 도메인에서는 이전과는 다른 HashCode값을 반환하게 된다.

     

     

     

     

     

    .NET Core에서의 String.GetHashCode()의 특징

    왜 String.GetHashCode()는 이렇게 동작하는 것일까? 그 내부를 한번 보도록 하자.

            public override int GetHashCode()
            {
                ulong seed = Marvin.DefaultSeed; // DefaultSeed 값이 static이므로 최초 실행 시 seed 값이 정해지고 고정됨.
                return Marvin.ComputeHash32(ref Unsafe.As<char, byte>(ref _firstChar), (uint)_stringLength * 2 /* in bytes, not chars */, (uint)seed, (uint)(seed >> 32));
            }
            public static ulong DefaultSeed { get; } = GenerateSeed(); // static
    
            private static unsafe ulong GenerateSeed()
            {
                ulong seed;
                Interop.GetRandomBytes((byte*)&seed, sizeof(ulong));
                return seed;
            }

    그 이유는 String.GetHashCode()를 최초 호출 시에 Hash를 수행하기 위한 Seed값이 결정되게 되고, 해당 Seed 값은 프로그램이 종료될 때까지 계속해서 사용되게 된다.

    그렇기에 프로그램을 새로 시작하여 String.GetHashCode()를 호출하게 되면 또 다른 Seed값이 정해지게 되어 프로그램마다 일관된 HashCode값을 반환하지 않는 것이다.

     

     

     

     

     

    .Net Framework에서의 String.GetHashCode()의 특징

    처음부터 String.GetHashCode()가 도메인에 따라 다른 HashCode값을 반환 했던 것은 아니다.

    .NET Framework에서는 String에 대해 항상 동일한 HashCode를 반환하는 것이 Default 이다. 그렇기에 .NET Core에서도 여전히 String.GetHashCode()가 항상 동일한 값을 반환할 것을 기대 하시는 계시는 분들이 있다.

    하지만 위에서도 설명했듯이 더 이상 그렇지 않다.

    다음 코드를 보자.

            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");

     

    FEATURE_RANDOMIZED_STRING_HASHING이라는 Define에 따라 String의 HashCode 값을 앱도메인 마다 다르게 할지 같게 할지를 결정할 수 있게 구현되어 있고 이 값은 App.config의 설정값으로 지정이 가능하다.

    <?xml version="1.0" encoding="utf-8" ?>
    <configuration>
        <startup> 
            <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
        </startup>
        <runtime>
          <UseRandomizedStringHashAlgorithm enabled="1" />
        </runtime>
    </configuration>

     

    .NET Core에 와서야 String의 HashCode값이 무조건 앱도메인마다 다르게 반환하도록 설정한 것인데, 이는 보안적인 이유에서 내려진 결정이다.

    Hash에서 계속 동일한 bucket에 값을 넣도록 유도하여 Hash Flooding을 발생 시킬 수 있고, 이러한 Hash Flooding은 Hash의 이점을 완전히 없애버리게 된다.

    이러한 상황은 ASP .NET에서 엄청난 성능 저하로 이어 질 수 있다.

    이에 대해 좀 더 자세한 내용은 다음 포스트와 스레드를 참고하도록 하자.

     

    https://github.com/dotnet/runtime/issues/26779
    https://andrewlock.net/why-is-string-gethashcode-different-each-time-i-run-my-program-in-net-core/

     

     

     

     

     

    결론

    Packet의 ID를 만들기 위해서 Packet의 Name에 String.GetHashCode()을 수행하는 것을 고려해 보다가 좀 더 알아 보게 된 내용이었다.

    결론적으로 String.GetHashCode()는 프로그램 실행 시마다 매번 달라 질 수 있다는 걸 유념하도록 하자.

     

     

     

    댓글