성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] How can I tell whether two programs...
[정성태] The case of the fail-fast crashes c...
[정성태] Creating Docker multi-arch images f...
[정성태] BinaryFormatter removed from .NET 9...
[정성태] Extending the Windows Shell Progres...
[우광현] 와..... 범위를 잡았으니 클라이언트가 해당 범위를 확인해본다...
[정성태] 딱히, 그것 이상으로 더 설명할 내용이 없습니다. 동적 포...
[정성태] If Windows 3.11 required a 32-bit p...
[정성태] What is a hard error, and what make...
[괴물신인] 질문작성자인데 이 글을 이제봤네요 ㄷㄷ 이 글처럼 타입별로 인...
글쓰기
제목
이름
암호
전자우편
HTML
홈페이지
유형
제니퍼 .NET
닷넷
COM 개체 관련
스크립트
VC++
VS.NET IDE
Windows
Team Foundation Server
디버깅 기술
오류 유형
개발 환경 구성
웹
기타
Linux
Java
DDK
Math
Phone
Graphics
사물인터넷
부모글 보이기/감추기
내용
<div style='display: inline'> <h1 style='font-family: Malgun Gothic, Consolas; font-size: 20pt; color: #006699; text-align: center; font-weight: bold'>C#에서 enum을 boxing 없이 int로 변환하기 - 두 번째 이야기</h1> <p> 이전 글에,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > C#에서 enum을 boxing 없이 int로 변환하기 ; <a target='tab' href='http://www.sysnet.pe.kr/2/0/11270'>http://www.sysnet.pe.kr/2/0/11270</a> </pre> <br /> 다음과 같은 덧글이 달렸군요.<br /> <br /> <div style='BACKGROUND-COLOR: #ccffcc; padding: 10px 10px 5px 10px; MARGIN: 0px 10px 10px 10px; FONT-FAMILY: Malgun Gothic, Consolas, Verdana; COLOR: #005555'> 참고하신 블로그의 다음 글로 https://libsora.so/posts/csharp-dictionary-enum-key-without-gc/ 이 올라왔는데요. 해당 글을 참고한다면 우회 방법을 사용하신 static Dictionary에서도 결국 박싱이 발생하는 것 아닐까요?<br /> </div><br /> <br /> 링크한 "C# Dictionary + enum (https://libsora.so/posts/csharp-dictionary-enum-key-without-gc)" 글을 보면 Dictionary.ContainsKey 메서드와 indexer에 enum 값을 전달하면 메서드 내부에서 호출되는 DefaultComparer.Equals와 DefaultComparer.GetHashCode의 메모리 할당 문제로 인해 결국 박싱이 일어난다는 것입니다. 왜냐하면, 제 코드에서도 어차피 Dictionary의 indexer를 이용한 접근을 하기 때문에,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 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[<span style='color: blue; font-weight: bold'>_enumKey[key]</span>]; } set { data[<span style='color: blue; font-weight: bold'>_enumKey[key]</span>] = value; } } } </pre> <br /> 박싱이 일어날 거라는 덧글입니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 그런데, 질문이 다소 잘못되었습니다. DefaultComparer.Equals와 DefaultComparer.GetHashCode 내부에서 어떤 작업을 하는지는 알 수 없으나 그것이 boxing인지, 다른 이유로 인해 발생하는 것인지 알 수 없기 때문입니다. 즉, 덧글의 질문은 다음과 같이 바뀌어야 합니다.<br /> <br /> <div style='BACKGROUND-COLOR: #ccffcc; padding: 10px 10px 5px 10px; MARGIN: 0px 10px 10px 10px; FONT-FAMILY: Malgun Gothic, Consolas, Verdana; COLOR: #005555'> 참고하신 블로그의 다음 글로 https://libsora.so/posts/csharp-dictionary-enum-key-without-gc/ 이 올라왔는데요. 해당 글을 참고한다면 우회 방법을 사용하신 static Dictionary에서도 <span style='color: blue; font-weight: bold'>결국 GC가 발생</span>하는 것 아닐까요? </div><br /> <br /> 그런데, 이건 유니티가 사용하는 Mono 플랫폼의 문제입니다. .NET 4.0 환경에서 테스트하면 인덱서 내부에서의 동작에 힙 할당이 전혀 발생하지 않습니다. 확인은 다음과 같이 할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 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) { <span style='color: blue; font-weight: bold'>Thread t = new Thread(reportGC); t.IsBackground = true; t.Start();</span> 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(); } } <span style='color: blue; font-weight: bold'> private static void reportGC() { while (true) { int count = GC.CollectionCount(0) + GC.CollectionCount(1) + GC.CollectionCount(2); Console.WriteLine(count); Thread.Sleep(1000); } }</span> 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; } } } } } </pre> <br /> 실행해 보면, GC가 전혀 발생하지 않습니다. 재미있는 것은 .NET 3.5로 빌드하면 이번에는 GC가 발생하는 것을 볼 수 있습니다. 즉, 내부 코드가 어떻게 작성되어 있느냐에 따라 Dictionary 타입의 indexer 사용 시 힙 할당 여부가 결정됩니다.<br /> <br /> 어쨌든 중요한 것은, 저 코드로 작성하게 되면 Unity3D 환경의 경우 GC가 발생하게 됩니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 그렇다면, WrapperObject 타입의 내부 컬렉션을 BCL의 Dictionary가 아닌, GC 힙을 할당하지 않는 사용자 정의 컬렉션으로 교체하면 어떨까요? 그런데, 이게 좀 재미있습니다. Dictionary와 같은 객체를 최소한의 구성으로 다음과 같이 만들어 보았는데요.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > class WrapperObject<TEnum, TValue> where TEnum : struct { TValue[] data; <span style='color: blue; font-weight: bold'>MyIntDict<TEnum> _enumKey = new MyIntDict<TEnum>();</span> 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 { <span style='color: blue; font-weight: bold'>int idx = key.GetHashCode();</span> return _data[idx]; } set { <span style='color: blue; font-weight: bold'>int idx = key.GetHashCode();</span> _data[idx] = value; } } } </pre> <br /> 단순히 key.GetHashCode() 만으로도 내부적으로 힙을 사용해 GC가 발생하게 됩니다. 그렇다면, 도대체 .NET 4.0의 Dictionary 타입은 어떻게 구현했길래 힙 메모리 사용이 없는 걸까요? 우선 indexer를 시작으로,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > [__DynamicallyInvokable] public TValue this[TKey key] { [__DynamicallyInvokable] get { int index = <span style='color: blue; font-weight: bold'>this.FindEntry</span>(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 = <span style='color: blue; font-weight: bold'>this.comparer.GetHashCode(key)</span> & 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; } </pre> <br /> 위의 코드에 사용된 this.comparer를 추적해 보면 특별히 enum 타입에 대해 RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter(...); 메서드를 이용해 동적으로 생성하고 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > [SecuritySafeCritical] private static EqualityComparer<T> CreateComparer() { // ...[생략]... <span style='color: blue; font-weight: bold'>if (c.IsEnum)</span> { 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); } } // ...[생략]... } </pre> <br /> 이것은 "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 타입 등으로 우회하고 싶어도 결국 동적 메서드 생성 이외에는 답이 없는 것처럼 보입니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 그런데, 갑자기 C#의 특수한 예약어가 생각났습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Fun With __makeref ; <a target='tab' href='http://benbowen.blog/post/fun_with_makeref/'>http://benbowen.blog/post/fun_with_makeref/</a> </pre> <br /> 그렇습니다. 저 예약어를 이용하면 enum 타입을 boxing 없이 int로 변경할 수 있습니다. 이렇게!<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 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로 가정 <span style='color: blue; font-weight: bold'>unsafe int ConvertToIndex(TEnum key) { System.TypedReference reference = __makeref(key); System.TypedReference* pRef = &reference; int* valuePtr = (int*)*((IntPtr*)&reference); return *valuePtr; }</span> /* int ConvertToIndex(TEnum key) { System.TypedReference reference = __makeref(key); return __refvalue(reference, int); // System.InvalidCastException: 'Specified cast is not valid.' } */ } </pre> <br /> 일단, Visual Studio와 Unity3d 개발 환경에서는 빌드 및 실행이 잘 됩니다. 단지, iOS 빌드를 위한 IL2CPP 환경에서 빌드/실행이 잘 되는지는 확인을 못했습니다. 그나저나, 설령 잘 된다고 해도, 저런 키워드를 써가면서까지 enum 타입을 (int)로 명시적인 형 변환을 필요 없게 만드는 것이 얼마나 큰 장점이 있을지는... 생각해 봐야 할 문제입니다. ^^<br /> <br /> (<a target='tab' href='http://www.sysnet.pe.kr/bbs/DownloadAttachment.aspx?fid=1247&boardid=331301885'>첨부 파일은 이 글의 예제 코드를 포함</a>합니다.)<br /> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
1299
(왼쪽의 숫자를 입력해야 합니다.)