Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 3개 있습니다.)
(시리즈 글이 5개 있습니다.)
.NET Framework: 673. C#에서 enum을 boxing 없이 int로 변환하기
; https://www.sysnet.pe.kr/2/0/11270

.NET Framework: 740. C#에서 enum을 boxing 없이 int로 변환하기 - 두 번째 이야기
; https://www.sysnet.pe.kr/2/0/11506

.NET Framework: 779. C# 7.3에서 enum을 boxing 없이 int로 변환하기 - 세 번째 이야기
; https://www.sysnet.pe.kr/2/0/11565

.NET Framework: 1042. C# - enum 값을 int로 암시적(implicit) 형변환하는 방법?
; https://www.sysnet.pe.kr/2/0/12606

.NET Framework: 2131. C# - Source Generator로 해결하는 enum 박싱 문제
; https://www.sysnet.pe.kr/2/0/13384




C#에서 enum을 boxing 없이 int로 변환하기 - 두 번째 이야기

이전 글에,

C#에서 enum을 boxing 없이 int로 변환하기
; https://www.sysnet.pe.kr/2/0/11270

다음과 같은 덧글이 달렸군요.

참고하신 블로그의 다음 글로 https://libsora.so/posts/csharp-dictionary-enum-key-without-gc/ 이 올라왔는데요. 해당 글을 참고한다면 우회 방법을 사용하신 static Dictionary에서도 결국 박싱이 발생하는 것 아닐까요?


링크한 "C# Dictionary + enum (https://libsora.so/posts/csharp-dictionary-enum-key-without-gc)" 글을 보면 Dictionary.ContainsKey 메서드와 indexer에 enum 값을 전달하면 메서드 내부에서 호출되는 DefaultComparer.Equals와 DefaultComparer.GetHashCode의 메모리 할당 문제로 인해 결국 박싱이 일어난다는 것입니다. 왜냐하면, 제 코드에서도 어차피 Dictionary의 indexer를 이용한 접근을 하기 때문에,

class WrapperObject<TEnum, TValue>
{
    TValue[] data;

    static Dictionary<TEnum, int> _enumKey = new Dictionary<TEnum, int>();

    ...[생략]...

    public WrapperObject(int count)
    {
        data = new TValue[count];
    }

    public TValue this[TEnum key]
    {
        get { return data[_enumKey[key]]; }
        set { data[_enumKey[key]] = value; }
    }
}

박싱이 일어날 거라는 덧글입니다.




그런데, 질문이 다소 잘못되었습니다. DefaultComparer.Equals와 DefaultComparer.GetHashCode 내부에서 어떤 작업을 하는지는 알 수 없으나 그것이 boxing인지, 다른 이유로 인해 발생하는 것인지 알 수 없기 때문입니다. 즉, 덧글의 질문은 다음과 같이 바뀌어야 합니다.

참고하신 블로그의 다음 글로 https://libsora.so/posts/csharp-dictionary-enum-key-without-gc/ 이 올라왔는데요. 해당 글을 참고한다면 우회 방법을 사용하신 static Dictionary에서도 결국 GC가 발생하는 것 아닐까요?


그런데, 이건 유니티가 사용하는 Mono 플랫폼의 문제입니다. .NET 4.0 환경에서 테스트하면 인덱서 내부에서의 동작에 힙 할당이 전혀 발생하지 않습니다. 확인은 다음과 같이 할 수 있습니다.

using System;
using System.Collections.Generic;
using System.Threading;

namespace ConsoleApp1
{
    class Program
    {
        interface IState
        {
            string GetMessage();
        }

        class State_Wait : IState
        {
            public string GetMessage()
            {
                return "wait";
            }
        }

        class State_Run : IState
        {
            public string GetMessage()
            {
                return "run";
            }
        }

        enum States
        {
            Wait,
            Run,
        }


        static void Main(string[] args)
        {
            Thread t = new Thread(reportGC);
            t.IsBackground = true;
            t.Start();

            WrapperObject<States, IState> states = new WrapperObject<States, IState>(2);
            states[States.Run] = new State_Wait();
            states[States.Wait] = new State_Run();

            while (true)
            {
                states[States.Run].GetMessage();
            }
        }

        private static void reportGC()
        {
            while (true)
            {
                int count = GC.CollectionCount(0) +
                    GC.CollectionCount(1) +
                    GC.CollectionCount(2);
                Console.WriteLine(count);

                Thread.Sleep(1000);
            }
        }

        class WrapperObject<TEnum, TValue> 
        {
            TValue[] data;

            static Dictionary<TEnum, int> _enumKey = new Dictionary<TEnum, int>();

            static WrapperObject()
            {
                int[] intValues = Enum.GetValues(typeof(TEnum)) as int[];
                TEnum[] enumValues = Enum.GetValues(typeof(TEnum)) as TEnum[];

                for (int i = 0; i < intValues.Length; i++)
                {
                    _enumKey.Add(enumValues[i], intValues[i]);
                }
            }

            public WrapperObject(int count)
            {
                data = new TValue[count];
            }

            public TValue this[TEnum key]
            {
                get { return data[_enumKey[key]]; }
                set { data[_enumKey[key]] = value; }
            }
        }
    }
}

실행해 보면, GC가 전혀 발생하지 않습니다. 재미있는 것은 .NET 3.5로 빌드하면 이번에는 GC가 발생하는 것을 볼 수 있습니다. 즉, 내부 코드가 어떻게 작성되어 있느냐에 따라 Dictionary 타입의 indexer 사용 시 힙 할당 여부가 결정됩니다.

어쨌든 중요한 것은, 저 코드로 작성하게 되면 Unity3D 환경의 경우 GC가 발생하게 됩니다.




그렇다면, WrapperObject 타입의 내부 컬렉션을 BCL의 Dictionary가 아닌, GC 힙을 할당하지 않는 사용자 정의 컬렉션으로 교체하면 어떨까요? 그런데, 이게 좀 재미있습니다. Dictionary와 같은 객체를 최소한의 구성으로 다음과 같이 만들어 보았는데요.

class WrapperObject<TEnum, TValue> where TEnum : struct
{
    TValue[] data;

    MyIntDict<TEnum> _enumKey = new MyIntDict<TEnum>();

    public WrapperObject(int count)
    {
        data = new TValue[count];
    }

    public TValue this[TEnum key]
    {
        get { return data[_enumKey[key]]; }
        set { data[_enumKey[key]] = value; }
    }
}

// 이 타입은 힙 메모리 사용을 없애기 위해 최소한의 사전형 구현체를 만든 것으로
// 너무 많은 가정을 포함하므로 현실적으로 사용할 수 없음.
class MyIntDict<TEnum> where TEnum : struct
{
    int[] _data;

    public MyIntDict()
    {
        int elemCount = Enum.GetValues(typeof(TEnum)).Length;
        _data = new int[elemCount];
    }

    // 혹시... key.GetHashCode 이외에 indexer로 전달된 값을 hash하는 방법이 있을까요?
    // 또는 꼭 사전 형식이 아니더라도 현실성 있게 heap 할당을 피할 수 있는 방법이 있을까요?
    public unsafe int this[TEnum key]
    {
        get
        {
            int idx = key.GetHashCode();
            return _data[idx];
        }

        set
        {
            int idx = key.GetHashCode();
            _data[idx] = value;
        }
    }
}

단순히 key.GetHashCode() 만으로도 내부적으로 힙을 사용해 GC가 발생하게 됩니다. 그렇다면, 도대체 .NET 4.0의 Dictionary 타입은 어떻게 구현했길래 힙 메모리 사용이 없는 걸까요? 우선 indexer를 시작으로,

[__DynamicallyInvokable]
public TValue this[TKey key]
{
    [__DynamicallyInvokable]
    get
    {
        int index = this.FindEntry(key);
        if (index >= 0)
        {
            return this.entries[index].value;
        }
        ThrowHelper.ThrowKeyNotFoundException();
        return default(TValue);
    }
    [__DynamicallyInvokable]
    set
    {
        this.Insert(key, value, false);
    }
}

private int FindEntry(TKey key)
{
    if (key == null)
    {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
    }
    if (this.buckets != null)
    {
        int num = this.comparer.GetHashCode(key) & 0x7fffffff;
        for (int i = this.buckets[num % this.buckets.Length]; i >= 0; i = this.entries[i].next)
        {
            if ((this.entries[i].hashCode == num) && this.comparer.Equals(this.entries[i].key, key))
            {
                return i;
            }
        }
    }
    return -1;
}

위의 코드에 사용된 this.comparer를 추적해 보면 특별히 enum 타입에 대해 RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter(...); 메서드를 이용해 동적으로 생성하고 있습니다.

[SecuritySafeCritical]
private static EqualityComparer<T> CreateComparer()
{
    // ...[생략]...
    if (c.IsEnum)
    {
        switch (Type.GetTypeCode(Enum.GetUnderlyingType(c)))
        {
            case TypeCode.SByte:
                return (EqualityComparer<T>) RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((RuntimeType) typeof(SByteEnumEqualityComparer<sbyte>), c);

            case TypeCode.Byte:
            case TypeCode.UInt16:
            case TypeCode.Int32:
            case TypeCode.UInt32:
                return (EqualityComparer<T>) RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((RuntimeType) typeof(EnumEqualityComparer<int>), c);

            case TypeCode.Int16:
                return (EqualityComparer<T>) RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((RuntimeType) typeof(ShortEnumEqualityComparer<short>), c);

            case TypeCode.Int64:
            case TypeCode.UInt64:
                return (EqualityComparer<T>) RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((RuntimeType) typeof(LongEnumEqualityComparer<long>), c);
        }
    }
    // ...[생략]...
}

이것은 "GC없이 C# Dictionary에서 enum을 key로 쓰기 (https://libsora.so/posts/csharp-dictionary-enum-key-without-gc)" 글에서 언급한 "Generic EnumComparer"와 같이 내부적으로 dynamic method 생성을 하는 식으로 처리하는 것과 방식이 유사합니다. 즉, .NET 4.0의 경우 enum의 경우까지도 고려해 동적으로 생성한 메서드 덕분에 GC 힙 사용을 피해 간 것입니다. 그렇다면, 사용자 정의 Dictionary 타입 등으로 우회하고 싶어도 결국 동적 메서드 생성 이외에는 답이 없는 것처럼 보입니다.




그런데, 갑자기 C#의 특수한 예약어가 생각났습니다.

Fun With __makeref
; http://benbowen.blog/post/fun_with_makeref/

그렇습니다. 저 예약어를 이용하면 enum 타입을 boxing 없이 int로 변경할 수 있습니다. 이렇게!

class WrapperObject<TEnum, TValue>
{
    TValue[] data;

    public WrapperObject(int count)
    {
        data = new TValue[count];
    }

    public TValue this[TEnum key]
    {
        get { return data[ConvertToIndex(key)]; }
        set { data[ConvertToIndex(key)] = value; }
    }

    // 이 코드는 enum의 기반 타입을 int로 가정
    unsafe int ConvertToIndex(TEnum key)
    {
        System.TypedReference reference = __makeref(key);
        System.TypedReference* pRef = &reference;

        int* valuePtr = (int*)*((IntPtr*)&reference);
        return *valuePtr;
    }

/*
    int ConvertToIndex(TEnum key)
    {
        System.TypedReference reference = __makeref(key);
        return __refvalue(reference, int); // System.InvalidCastException: 'Specified cast is not valid.'
    }
*/
}

일단, Visual Studio와 Unity3d 개발 환경에서는 빌드 및 실행이 잘 됩니다. 단지, iOS 빌드를 위한 IL2CPP 환경에서 빌드/실행이 잘 되는지는 확인을 못했습니다. 그나저나, 설령 잘 된다고 해도, 저런 키워드를 써가면서까지 enum 타입을 (int)로 명시적인 형 변환을 필요 없게 만드는 것이 얼마나 큰 장점이 있을지는... 생각해 봐야 할 문제입니다. ^^

(첨부 파일은 이 글의 예제 코드를 포함합니다.)




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 6/22/2023]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2018-06-21 09시57분
[netpyoung] 신기한 키워드네요. 흑마법이라 일단 테스트를 해보는데 다른 결과가 나왔네요.
윈도우즈 netcoreapp2.0 환경에서는 잘 동작하나,
윈도우즈 Unity 2017.4.2f2 - .NET 4.6 - 5.0.1 (Visual Studio built mono) 에서는
__makeref(E_Hello.A) == __makeref(E_Hello.B) == __makeref(E_Hello.C)처럼,
enum에 대해 값이 동일한 값을 반환하도록 되어 있네요.
[guest]
2018-06-26 06시54분
[netpyoung] unsafe
        {
            TEnum a = TEnum.A;
            TEnum b = TEnum.B;
            TypedReference refA = __makeref(a);
            TypedReference refB = __makeref(b);


            int* valuePtrA = (int*) *((IntPtr*) &refA);
            int* valuePtrB = (int*) *((IntPtr*) &refB);
            int expectedA = *valuePtrA;
            int expectedB = *valuePtrB;
            Debug.Log(expectedA == expectedB);
        }
[guest]
2018-06-27 01시58분
보니까, mono 런타임이 문제입니다. 모노로 빌드한,

C:\temp>dmcs Program.cs /unsafe

Program.exe를 그냥 실행시키면(즉, 시스템에 설치된 .NET Framework 위에서는 expectedA, expectedB 값을 0과 1로 잘 가져옵니다.

반면 다음과 같이 mono 런타임에 얹어서 실행하면,

c:\temp>mono Program.exe

이제는 expectedA, expectedB 값이 모두 이상한 값(예를 들어, -402205784)으로 나옵니다. 값이 실행할 때마다 바뀌는 걸로 봐서 그 순간에 메모리 상에 있는 쓰레기 값이 나오는 것 같습니다.
정성태
2018-06-27 02시05분
@netpyoung 님, 다음의 글에 간단하게 정리해 봤습니다.

(Unity가 사용하는) 모노 런타임의 __makeref 오류
; http://www.sysnet.pe.kr/2/0/11564

아울러, 테스트 덧글 달아주신 것 감사드립니다. ^^
정성태
2018-06-28 09시46분
C# 7.3에서 enum을 boxing 없이 int 변환하기 - 세 번째 이야기
; http://www.sysnet.pe.kr/2/0/11565
정성태
2018-06-28 11시20분
[netpyoung]
Unity 2017.4.2f2 - .NET 4.6 - 5.0.1 (Visual Studio built mono)
유니티 프로파일러를 돌려보니, 기존 (쓰레기가 할당되는) DefaultComparer.Equals가 => EnumEqualityComparer로 바뀌어서(일단 에디터상으로는) GC가 할당되야 하는데 할당이 안되도록 패치되어 있네요.
안되면 ZeroFormatter같은 경우는 CreateDelegate를 이용하여 회피하는 방법을 선택했더군요..(https://github.com/neuecc/ZeroFormatter/blob/master/src/ZeroFormatter/Comparers/EnumEqualityComparer.cs)

별도로 또 테스트를 하셨다니ㅠ. 자주 챙겨보고 있습니다. 이 자리를 빌려 감사드립니다.
[guest]
2020-02-21 05시42분
[지존] 엄청나네요 ㅋㅋㅋㅋ
[guest]

1  2  3  4  5  6  7  8  9  [10]  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13373정성태6/19/20234398오류 유형: 865. 파이썬 - pymssql 설치 관련 오류 정리
13372정성태6/15/20233110개발 환경 구성: 682. SQL Server TLS 통신을 위해 사용되는 키 길이 확인 방법
13371정성태6/15/20233132개발 환경 구성: 681. openssl - 인증서 버전(V1 / V3)
13370정성태6/14/20233295개발 환경 구성: 680. C# - Ubuntu + Microsoft.Data.SqlClient + SQL Server 2008 R2 연결 방법 - TLS 1.2 지원
13369정성태6/13/20233092개발 환경 구성: 679. PyCharm(을 비롯해 JetBrains에 속한 여타) IDE에서 내부 Window들의 탭이 없어진 경우
13368정성태6/13/20233225개발 환경 구성: 678. openssl로 생성한 인증서를 SQL Server의 암호화 인증서로 설정하는 방법
13367정성태6/10/20233329오류 유형: 864. openssl로 만든 pfx 인증서를 Windows Server 2016 이하에서 등록 시 "The password you entered is incorrect" 오류 발생
13366정성태6/10/20233130.NET Framework: 2128. C# - 윈도우 시스템에서 지원하는 암호화 목록(Cipher Suites) 나열파일 다운로드1
13365정성태6/8/20232895오류 유형: 863. MODIFY FILE encountered operating system error 112(failed to retrieve text for this error. Reason: 15105)
13364정성태6/8/20233677.NET Framework: 2127. C# - Ubuntu + Microsoft.Data.SqlClient + SQL Server 2008 R2 연결 방법 [1]
13363정성태6/7/20233241스크립트: 49. 파이썬 - "Transformers (신경망 언어모델 라이브러리) 강좌" - 1장 2절 코드 실행 결과
13362정성태6/1/20233164.NET Framework: 2126. C# - 서버 측의 요청 제어 (Microsoft.AspNetCore.RateLimiting)파일 다운로드1
13361정성태5/31/20233638오류 유형: 862. Facebook - ASP.NET/WebClient 사용 시 graph.facebook.com/me 호출에 대해 403 Forbidden 오류
13360정성태5/31/20233036오류 유형: 861. WSL/docker - failed to start shim: start failed: io.containerd.runc.v2: create new shim socket
13359정성태5/19/20233352오류 유형: 860. Docker Desktop - k8s 초기화 무한 반복한다면?
13358정성태5/17/20233660.NET Framework: 2125. C# - Semantic Kernel의 Semantic Memory 사용 예제 [1]파일 다운로드1
13357정성태5/16/20233464.NET Framework: 2124. C# - Semantic Kernel의 Planner 사용 예제파일 다운로드1
13356정성태5/15/20233769DDK: 10. Device Driver 테스트 설치 관련 오류 (Code 37, Code 31) 및 인증서 관련 정리
13355정성태5/12/20233685.NET Framework: 2123. C# - Semantic Kernel의 ChatGPT 대화 구현 [1]파일 다운로드1
13354정성태5/12/20233954.NET Framework: 2122. C# - "Use Unicode UTF-8 for worldwide language support" 설정을 한 경우, 한글 입력이 '\0' 문자로 처리
13352정성태5/12/20233569.NET Framework: 2121. C# - Semantic Kernel의 대화 문맥 유지파일 다운로드1
13351정성태5/11/20234069VS.NET IDE: 185. Visual Studio - 원격 Docker container 내에 실행 중인 응용 프로그램에 대한 디버깅 [1]
13350정성태5/11/20233319오류 유형: 859. Windows Date and Time - Unable to continue. You do not have permission to perform this task
13349정성태5/11/20233660.NET Framework: 2120. C# - Semantic Kernel의 Skill과 Function 사용 예제파일 다운로드1
13348정성태5/10/20233570.NET Framework: 2119. C# - Semantic Kernel의 "Basic Loading of the Kernel" 예제
13347정성태5/10/20233931.NET Framework: 2118. C# - Semantic Kernel의 Prompt chaining 예제파일 다운로드1
1  2  3  4  5  6  7  8  9  [10]  11  12  13  14  15  ...