유니티 최적화 관련해서 이런 얘기가 있다.

'Dictionary에 Enum을 Key로 사용하면 내부적으로 박싱이 일어나기 때문에, 사용해선 안된다.'

 

나도 최근에야 친한 지인분에게 이 얘기를 들었고 처음에 충격 먹었다. 여태까지 잘만 사용하고 있었으니까... :O

 

일단, 왜 사용하면 안 될까를 살펴보면 이와 같다.


Dictionary는 키값이 같은지 여부를 판단할 때, 내부적으로 IEqualityComparer를 사용하는데, 만약 따로 생성자로 IEqualityComparer를 전달해주지 않으면 Dictionary 내부에서 Property 멤버인 EqualityComparer.Default를 호출한다.

이 Property는 내부적으로 Key 타입에 따라 적절한 Comparer를 생성해서 리턴하는 CreateComparer 메소드를 호출하는데, Enum에 경우엔 적절한 Comparer가 없어서 기본 타입인 ObjectEqualityComparer를 리턴한다.

문제는 이 ObjectEqualityComparer 녀석은 이름처럼 Object를 사용하는 녀석이라, Enum이 키값일 때 Object로 형 변환하는 박싱이 일어난다. (Enum은 숨겨진 타입으로 int를 상속하는 ValueType이다.)


그런데 문득 정말 박싱이 일어나는지 궁금해져 테스트를 해보기로 했다.

(테스트 버전: Unity 2018 LTS)

 

테스트 코드는 단순하다.

public class EnumTest : MonoBehaviour
{
    private Dictionary<EnumKey, int> _dicEnum = new Dictionary<EnumKey, int>();

    private void Update()
    {
        Profiler.BeginSample("Test Enum");
        _dicEnum.Add(EnumKey.One, 1);
        _dicEnum.Add(EnumKey.Two, 2);
        _dicEnum.Add(EnumKey.Three, 3);

        _dicEnum.Remove(EnumKey.One);
        _dicEnum.Remove(EnumKey.Two);
        _dicEnum.Remove(EnumKey.Three);
        Profiler.EndSample();
    }

    enum EnumKey
    {
        One,
        Two,
        Three
    }
}

매 프레임마다, Add와 Remove를 반복해서 이를 프로파일러에서 확인할 수 있게 하는 것.

 

테스트 결과는 이렇게 나왔다.

...?! 기대한 것과는 다르게 GC Alloc이 0이다. 박싱이 일어나지 않았다는 뜻이다.

혹시 이전 유니티 버전에서 해결된 건 아닌지, 어떤 건지 2017 버전도 테스트해보다가 Script Runtime Version을 바꾸고 나서, 유의미한 결과 값을 얻을 수 있었다.

 

.NET Framework의 버전을 3,5로 바꾸면?

이번엔 가비지가 생겼다.

이 말인즉, 닷넷이 4.0으로 업데이트 되면서 방식 문제가 해결됐다는 뜻이다.

 

어떻게 해결된 것일까 찾아보던 중, 한 분의 블로그를 보고 해답을 찾을 수 있었다.


...

 

4 버전대 이상 닷넷에선 EqualityComparer.CreateComparer()의 로직이 바뀌었고, 이젠 타입이 enum인지 아닌지를 봐서 EnumEqualityComparer라는 전용 비교자를 만들어 넘겨준다. 진짜 그런지 한번 들여다본다

 

...

출처: https://enghqii.tistory.com/69 [그냥저냥 휴학생]

 

C#에서 Dictionary에 Enum을 써도 괜찮은것 같다

전에 이런 글에서 Dictionary에 Key값으로 enum을 넣으면 내부에서 boxing이 일어나는데, 그 이유는 Dictionary 내부에서 IEqualityComarer로 ObjectEqualityComparer를 사용하게 되기 때문이라고 했다. 4 버전대..

enghqii.tistory.com


다시 말하자면, .NET 4.0 버전 이상에서는 로직이 개선돼서, 더 이상 Enum 타입의 Comparer를 구할 때, ObjectEqualityComparer가 아닌, 박싱이 일어나지 않는 EnumEqualityComparer 타입이 사용된다는 것이다.

실제로 블로거분이 걸어준 mscorlib 안의 코드를 볼 수 있는 사이트로 가서 CreateComparer 함수 구현을 보면, 다음과 같이 if 문으로 Enum 타입을 체크하고 Enum을 위한 Comparer를 리턴하는 것을 볼 수 있다.

// See the METHOD__JIT_HELPERS__UNSAFE_ENUM_CAST and METHOD__JIT_HELPERS__UNSAFE_ENUM_CAST_LONG cases in getILIntrinsicImplementation
if (t.IsEnum) {
    TypeCode underlyingTypeCode = Type.GetTypeCode(Enum.GetUnderlyingType(t));
 
    // Depending on the enum type, we need to special case the comparers so that we avoid boxing
    // Note: We have different comparers for Short and SByte because for those types we need to make sure we call GetHashCode on the actual underlying type as the 
    // implementation of GetHashCode is more complex than for the other types.
    switch (underlyingTypeCode) {
        case TypeCode.Int16: // short
            return (EqualityComparer<T>) RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((RuntimeType)typeof(ShortEnumEqualityComparer<short>), t);
        case TypeCode.SByte:
            return (EqualityComparer<T>) RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((RuntimeType)typeof(SByteEnumEqualityComparer<sbyte>), t);
        case TypeCode.Int32:
        case TypeCode.UInt32:
        case TypeCode.Byte:
        case TypeCode.UInt16: //ushort
            return (EqualityComparer<T>) RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((RuntimeType)typeof(EnumEqualityComparer<int>), t);
        case TypeCode.Int64:
        case TypeCode.UInt64:
            return (EqualityComparer<T>) RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((RuntimeType)typeof(LongEnumEqualityComparer<long>), t);
    }
}

https://referencesource.microsoft.com/#mscorlib/system/collections/generic/equalitycomparer.cs,d8e28972e89a3e86

 

그러니 결과는..?

이제는 사용해도 무방하다!

 

Unity 2018부터는 기본적으로 4.x 버전을 사용하도록 하고 있다. 심지어 2019 버전에서는 아예 '.NET 3.5'는 삭제되고, '.NET 4.X'와 '.NET Standard 2.0'만 있으니, 억지로 2017, 2018 버전으로 가서 3.5 버전으로 다운그레이드 하지 않는 이상 이런 걱정은 하지 않아도 된다는 말이다.

 

실제로 위 프로파일러 스샷에 보여주지 않았지만, Deep Profile로 설정해 놓고 Dictionary.Add와 Remove 항목을 펼쳐보면,

실제로 내부적으로 EnumEqualityComparer를 사용하는 것을 확인할 수 있다.

 

 

[추가 정보]

위의 코드를 보면 단순히 Enum타입만 체크해서 EnumEqualityComparer를 리턴하는 게 아닌, 다양한 Primitive Data Type을 체크해서 알맞게 리턴하는 것을 볼 수 있는데. 이유에 대해 설명하자면, 앞서 박싱이 일어나는 이유에 대해 설명할 때 언급했듯이. Enum은 숨겨진 타입(UnderlyingType)으로 int32를 상속하기 때문이다.

하지만 개발자가 원하면 Enum에 int가 아닌 short(int16) 같은 다른 integer type을 상속받게 할 수 있기 때문에 위에서 Comparer를 리턴할 때, 숨겨진 타입이 뭔지 하나씩 체크하는 것이다.

 

p.s. 2019년까지도 블로그나 심지어 Unity 2019.03 버전 매뉴얼에도(!!) Dictionary의 Key로 Enum을 쓰지 말라는 자료가 수두룩 한데, 실제로 문제가 수정된 건 2017 버전부터 이다. 이제는 최신 정보로 업데이트 돼야할 때가 되지 않았나 싶다.

+ Recent posts