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]

... 31  32  33  34  35  36  [37]  38  39  40  41  42  43  44  45  ...
NoWriterDateCnt.TitleFile(s)
12695정성태7/3/20217962VC++: 145. C 언어의 setjmp/longjmp 기능을 Thread Context를 이용해 유사하게 구현하는 방법파일 다운로드1
12694정성태7/2/20219870Java: 24. Azure - Spring Boot 앱을 Java SE(Embedded Web Server)로 호스팅 시 로그 파일 남기는 방법 [1]
12693정성태6/30/20217646오류 유형: 731. Azure Web App Site Extension - Failed to install web app extension [...]. {1}
12692정성태6/30/20217518디버깅 기술: 180. Azure - Web App의 비정상 종료 시 남겨지는 로그 확인
12691정성태6/30/20218341개발 환경 구성: 573. 테스트 용도이지만 테스트에 적합하지 않은 Azure D1 공유(shared) 요금제
12690정성태6/28/20219127Java: 23. Azure - 자바(Java)로 만드는 Web App Service - Tomcat 호스팅
12689정성태6/25/20219693오류 유형: 730. Windows Forms 디자이너 - The class Form1 can be designed, but is not the first class in the file. [1]
12688정성태6/24/20219403.NET Framework: 1073. C# - JSON 역/직렬화 시 리플렉션 손실을 없애는 JsonSrcGen [2]파일 다운로드1
12687정성태6/22/20217424오류 유형: 729. Invalid data: Invalid artifact, java se app service only supports .jar artifact
12686정성태6/21/20219815Java: 22. Azure - 자바(Java)로 만드는 Web App Service - Java SE (Embedded Web Server) 호스팅
12685정성태6/21/202110028Java: 21. Azure Web App Service에 배포된 Java 프로세스의 메모리 및 힙(Heap) 덤프 뜨는 방법
12684정성태6/19/20218489오류 유형: 728. Visual Studio 2022부터 DTE.get_Properties 속성 접근 시 System.MissingMethodException 예외 발생
12683정성태6/18/20219929VS.NET IDE: 166. Visual Studio 2022 - Windows Forms 프로젝트의 x86 DLL 컨트롤이 Designer에서 오류가 발생하는 문제 [1]파일 다운로드1
12682정성태6/18/20217715VS.NET IDE: 165. Visual Studio 2022를 위한 Extension 마이그레이션
12681정성태6/18/20217035오류 유형: 727. .NET 2.0 ~ 3.5 + x64 환경에서 System.EnterpriseServices 참조 시 CS8012 경고
12680정성태6/18/20218120오류 유형: 726. python2.7.exe 실행 시 0xc000007b 오류
12679정성태6/18/20218725COM 개체 관련: 23. CoInitializeSecurity의 전역 설정을 재정의하는 CoSetProxyBlanket 함수 사용법파일 다운로드1
12678정성태6/17/20217998.NET Framework: 1072. C# - CoCreateInstance 관련 Inteop 오류 정리파일 다운로드1
12677정성태6/17/20219433VC++: 144. 역공학을 통한 lxssmanager.dll의 ILxssSession 사용법 분석파일 다운로드1
12676정성태6/16/20219504VC++: 143. ionescu007/lxss github repo에 공개된 lxssmanager.dll의 CLSID_LxssUserSession/IID_ILxssSession 사용법파일 다운로드1
12675정성태6/16/20217551Java: 20. maven package 명령어 결과물로 (war가 아닌) jar 생성 방법
12674정성태6/15/20218303VC++: 142. DEFINE_GUID 사용법
12673정성태6/15/20219467Java: 19. IntelliJ - 자바(Java)로 만드는 Web App을 Tomcat에서 실행하는 방법
12672정성태6/15/202110584오류 유형: 725. IntelliJ에서 Java webapp 실행 시 "Address localhost:1099 is already in use" 오류
12671정성태6/15/202117241오류 유형: 724. Tomcat 실행 시 Failed to initialize connector [Connector[HTTP/1.1-8080]] 오류
12670정성태6/13/20218840.NET Framework: 1071. DLL Surrogate를 이용한 Out-of-process COM 개체에서의 CoInitializeSecurity 문제파일 다운로드1
... 31  32  33  34  35  36  [37]  38  39  40  41  42  43  44  45  ...