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)
13585정성태3/26/20241499Windows: 262. PerformanceCounter의 InstanceName에 pid를 추가한 "Process V2"
13584정성태3/26/20241460개발 환경 구성: 708. Unity3D - C# Windows Forms / WPF Application에 통합하는 방법파일 다운로드1
13583정성태3/25/20241492Windows: 261. CPU Utilization이 100% 넘는 경우를 성능 카운터로 확인하는 방법
13582정성태3/19/20241583Windows: 260. CPU 사용률을 나타내는 2가지 수치 - 사용량(Usage)과 활용률(Utilization)파일 다운로드1
13581정성태3/18/20241760개발 환경 구성: 707. 빌드한 Unity3D 프로그램을 C++ Windows Application에 통합하는 방법
13580정성태3/15/20241293닷넷: 2231. C# - ReceiveTimeout, SendTimeout이 적용되지 않는 Socket await 비동기 호출파일 다운로드1
13579정성태3/13/20241500오류 유형: 899. HTTP Error 500.32 - ANCM Failed to Load dll
13578정성태3/11/20241645닷넷: 2230. C# - 덮어쓰기 가능한 환형 큐 (Circular queue)파일 다운로드1
13577정성태3/9/20241916닷넷: 2229. C# - 닷넷을 위한 난독화 도구 소개 (예: ConfuserEx)
13576정성태3/8/20241580닷넷: 2228. .NET Profiler - IMetaDataEmit2::DefineMethodSpec 사용법
13575정성태3/7/20241700닷넷: 2227. 최신 C# 문법을 .NET Framework 프로젝트에 쓸 수 있을까요?
13574정성태3/6/20241571닷넷: 2226. C# - "Docker Desktop for Windows" Container 환경에서의 IPv6 DualMode 소켓
13573정성태3/5/20241578닷넷: 2225. Windbg - dumasync로 분석하는 async/await 호출
13572정성태3/4/20241664닷넷: 2224. C# - WPF의 Dispatcher Queue로 알아보는 await 호출의 hang 현상파일 다운로드1
13571정성태3/1/20241638닷넷: 2223. C# - await 호출과 WPF의 Dispatcher Queue 동작 확인파일 다운로드1
13570정성태2/29/20241648닷넷: 2222. C# - WPF의 Dispatcher Queue 동작 확인파일 다운로드1
13569정성태2/28/20241553닷넷: 2221. C# - LoadContext, LoadFromContext 그리고 GAC파일 다운로드1
13568정성태2/27/20241619닷넷: 2220. C# - .NET Framework 프로세스의 LoaderOptimization 설정을 확인하는 방법파일 다운로드1
13567정성태2/27/20241626오류 유형: 898. .NET Framework 3.5 이하에서 mscoree.tlb 참조 시 System.BadImageFormatException파일 다운로드1
13566정성태2/27/20241638오류 유형: 897. Windows 7 SDK 설치 시 ".NET Development" 옵션이 비활성으로 선택이 안 되는 경우
13565정성태2/23/20241625닷넷: 2219. .NET CLR2 보안 모델에서의 개별 System.Security.Permissions 제어
13564정성태2/22/20241911Windows: 259. Hyper-V Generation 1 유형의 VM을 Generation 2 유형으로 바꾸는 방법
13563정성태2/21/20241949디버깅 기술: 196. windbg - async/await 비동기인 경우 메모리 덤프 분석의 어려움
13562정성태2/21/20241960오류 유형: 896. ASP.NET - .NET Framework 기본 예제에서 System.Web에 대한 System.IO.FileNotFoundException 예외 발생
13561정성태2/20/20242035닷넷: 2218. C# - (예를 들어, Socket) 비동기 I/O에 대한 await 호출 시 CancellationToken을 이용한 취소파일 다운로드1
13560정성태2/19/20242055디버깅 기술: 195. windbg 분석 사례 - Semaphore 잠금으로 인한 Hang 현상 (닷넷)
1  [2]  3  4  5  6  7  8  9  10  11  12  13  14  15  ...