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)
13270정성태2/24/20233755.NET Framework: 2098. dotnet build에 /p 옵션을 적용 시 유의점
13269정성태2/23/20234290스크립트: 46. 파이썬 - uvicorn의 콘솔 출력을 UDP로 전송
13268정성태2/22/20234842개발 환경 구성: 667. WSL 2 내부에서 열고 있는 UDP 서버를 호스트 측에서 접속하는 방법
13267정성태2/21/20234770.NET Framework: 2097. C# - 비동기 소켓 사용 시 메모리 해제가 finalizer 단계에서 발생하는 사례파일 다운로드1
13266정성태2/20/20234371오류 유형: 848. .NET Core/5+ - Process terminated. Couldn't find a valid ICU package installed on the system
13265정성태2/18/20234285.NET Framework: 2096. .NET Core/5+ - PublishSingleFile 유형에 대한 runtimeconfig.json 설정
13264정성태2/17/20235769스크립트: 45. 파이썬 - uvicorn 사용자 정의 Logger 작성
13263정성태2/16/20233910개발 환경 구성: 666. 최신 버전의 ilasm.exe/ildasm.exe 사용하는 방법
13262정성태2/15/20235003디버깅 기술: 191. dnSpy를 이용한 (소스 코드가 없는) 닷넷 응용 프로그램 디버깅 방법 [1]
13261정성태2/15/20234295Windows: 224. Visual Studio - 영문 폰트가 Fullwidth Latin Character로 바뀌는 문제
13260정성태2/14/20234079오류 유형: 847. ilasm.exe 컴파일 오류 - error : syntax error at token '-' in ... -inf
13259정성태2/14/20234208.NET Framework: 2095. C# - .NET5부터 도입된 CollectionsMarshal
13258정성태2/13/20234104오류 유형: 846. .NET Framework 4.8 Developer Pack 설치 실패 - 0x81f40001
13257정성태2/13/20234196.NET Framework: 2094. C# - Job에 Process 포함하는 방법 [1]파일 다운로드1
13256정성태2/10/20235045개발 환경 구성: 665. WSL 2의 네트워크 통신 방법 - 두 번째 이야기
13255정성태2/10/20234344오류 유형: 845. gihub - windows2022 이미지에서 .NET Framework 4.5.2 미만의 프로젝트에 대한 빌드 오류
13254정성태2/10/20234251Windows: 223. (WMI 쿼리를 위한) PowerShell 문자열 escape 처리
13253정성태2/9/20234991Windows: 222. C# - 다른 윈도우 프로그램이 실행되었음을 인식하는 방법파일 다운로드1
13252정성태2/9/20233821오류 유형: 844. ssh로 명령어 수행 시 멈춤 현상
13251정성태2/8/20234299스크립트: 44. 파이썬의 3가지 스레드 ID
13250정성태2/8/20236112오류 유형: 843. System.InvalidOperationException - Unable to configure HTTPS endpoint
13249정성태2/7/20234908오류 유형: 842. 리눅스 - You must wait longer to change your password
13248정성태2/7/20234044오류 유형: 841. 리눅스 - [사용자 계정] is not in the sudoers file. This incident will be reported.
13247정성태2/7/20234961VS.NET IDE: 180. Visual Studio - 닷넷 소스 코드 디버깅 중 "Decompile source code"가 동작하는 않는 문제
13246정성태2/6/20234073개발 환경 구성: 664. Hyper-V에 설치한 리눅스 VM의 VHD 크기 늘리는 방법 - 두 번째 이야기
13245정성태2/6/20234618.NET Framework: 2093. C# - PEM 파일을 이용한 RSA 개인키/공개키 설정 방법파일 다운로드1
1  2  3  4  5  6  7  8  9  10  11  12  13  [14]  15  ...