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]

... 166  167  168  169  170  171  172  [173]  174  175  176  177  178  179  180  ...
NoWriterDateCnt.TitleFile(s)
679정성태3/15/200920096오류 유형: 72. IE 8 멈춤 현상 - 두 번째 이야기
678정성태3/15/200925301개발 환경 구성: 37. Hyper-V에서 Vista의 Virtual Machine Bus 장치 인식 문제
677정성태3/15/200925793개발 환경 구성: 36. MSI P45 Neo3-FR V2 - RAID 1 구성
997정성태2/26/201121050    답변글 개발 환경 구성: 36.1. 개발 환경 구성: 34 - 1. RAID 1 구성 이후...
676정성태3/15/200922411오류 유형: 71. IE 8 RC1 - 멈춤 현상
675정성태3/14/200925309오류 유형: 70. Virtual Machine Additions 제거
673정성태3/11/200923904웹: 10. 금일 진행되었던 마이크로소프트 IE 8 행사의 ActiveX 세션 PPT [6]파일 다운로드1
672정성태3/1/200936901Windows: 43. Loopback network adapter 설치하기 [2]
670정성태2/22/200930055.NET Framework: 124. IIS 7에서 SVC 호스팅 [1]
669정성태2/21/200924470오류 유형: 69. The SQL Server Network Interface library could not register the Service Principal Name (SPN) for the SQL Server service.
668정성태2/21/200929534개발 환경 구성: 35. SQL Express 버전과 User Instance 옵션
667정성태2/14/200941474웹: 9. IE 7. IWebBrowser2 인터페이스 메서드의 Navigate / Navigate2 차이점 [3]파일 다운로드1
666정성태2/12/200927280웹: 8. IE 8: 프로세스와 윈도우의 관계 - LCIE [1]파일 다운로드1
665정성태2/7/200924203웹: 7. Internet Explorer 8 - XHR, XDR, XST, XSRF [1]
664정성태2/5/200920272Windows: 42. Concurrency Runtime
663정성태2/5/200924963.NET Framework: 123. WPF - Arial Narrow 폰트 문제
662정성태2/3/200920654VS.NET IDE: 59. HyperAddin 도구 사용 설명 [1]
661정성태2/3/200924347오류 유형: 68. msxml6r.dll 설치 오류
660정성태2/3/200922973Windows: 41. UAC 보안 취약 [2]
659정성태2/2/200934306오류 유형: 67. ClickOnce 응용 프로그램이 실행되지 않을 때.
658정성태2/1/200926319Team Foundation Server: 30. 소스 서버 보안
657정성태2/1/200929244Windows: 40. Q1 Ultra + Windows 7 베타
656정성태2/1/200930750디버깅 기술: 24. .NET JIT 최적화 코드 생성 제어
655정성태1/31/200926990Windows: 39. IE8 표준 모드 [4]
653정성태1/29/200921891.NET Framework: 122. XML Serializer를 이용한 값 복사: 성능은 어떨까!파일 다운로드1
652정성태1/22/200922723.NET Framework: 121. WPF - PrintTicket provider failed to bind to printer.
... 166  167  168  169  170  171  172  [173]  174  175  176  177  178  179  180  ...