오늘 세션 강의에서 유니티 최적화방법에 관하여 여러가지를 배웠다.
그 중 박싱 언박싱에 관한 내용인데 'Dictionary에 Enum을 Key로 사용하면 내부적으로 박싱이 일어나기 때문에, 사용해선 안된다.'라는 부분에 .Net4.0이상에서는 사용해도 괜찮다는 말이 있었다.
오늘은 이부분에 대하여 간략하게 정리하고 넘어가려고 한다.
참고자료: https://pizzasheepsdev.tistory.com/2
Dictionary는 키값이 같은지 여부를 판단할 때, 내부적으로 IEqualityComparer를 사용하는데, 만약 따로 생성자로 IEqualityComparer를 전달해주지 않으면 Dictionary 내부에서 Property 멤버인 EqualityComparer.Default를 호출한다.
이 Property는 내부적으로 Key 타입에 따라 적절한 Comparer를 생성해서 리턴하는 CreateComparer 메소드를 호출하는데, Enum에 경우엔 적절한 Comparer가 없어서 기본 타입인 ObjectEqualityComparer를 리턴한다.
문제는 이 ObjectEqualityComparer 녀석은 이름처럼 Object를 사용하는 녀석이라, Enum이 키값일 때 Object로 형 변환하는 박싱이 일어난다. (Enum은 숨겨진 타입으로 int를 상속하는 ValueType이다.)
4 버전대 이상 닷넷에선 EqualityComparer.CreateComparer()의 로직이 바뀌었고, 이젠 타입이 enum인지 아닌지를 봐서 EnumEqualityComparer라는 전용 비교자를 만들어 넘겨준다
코드를 확인해 보자
우선 underlyingTypeCode를 불러오는데 Enum.GetUnderlyingType(t)에서 t가 Enum인 경우 숨겨진 Type코드를 불러오고 GetTypeCode에서 Enum값으로 타입을 저장한다.
스위치문으로 저장된 Type에 따라 Type에 맞는 EnumEqualityComparer를 호출한다.
private static EqualityComparer<T> CreateComparer() {
Contract.Ensures(Contract.Result<EqualityComparer<T>>() != null);
RuntimeType t = (RuntimeType)typeof(T);
// Specialize type byte for performance reasons
if (t == typeof(byte)) {
return (EqualityComparer<T>)(object)(new ByteEqualityComparer());
}
// If T implements IEquatable<T> return a GenericEqualityComparer<T>
if (typeof(IEquatable<T>).IsAssignableFrom(t)) {
return (EqualityComparer<T>)RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((RuntimeType)typeof(GenericEqualityComparer<int>), t);
}
// If T is a Nullable<U> where U implements IEquatable<U> return a NullableEqualityComparer<U>
if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Nullable<>)) {
RuntimeType u = (RuntimeType)t.GetGenericArguments()[0];
if (typeof(IEquatable<>).MakeGenericType(u).IsAssignableFrom(u)) {
return (EqualityComparer<T>)RuntimeTypeHandle.CreateInstanceForAnotherGenericParameter((RuntimeType)typeof(NullableEqualityComparer<int>), u);
}
}
// 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));
// 우선 underlyingTypeCode를 불러오는데 Enum.GetUnderlyingType(t)에서 t가 Enum인 경우 숨겨진 Type코드를 불러오고 GetTypeCode에서 Enum값으로 타입을 저장한다.
// Depending on the enum type, we need to special case the comparers so that we avoid boxing
// enum type에서 box을 피하기 위해 특별한 comparers가 필요하단다.
// 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: // 스위치문으로 저장된 Type에 따라 Type에 맞는 EnumEqualityComparer를 호출한다.
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);
}
}
// Otherwise return an ObjectEqualityComparer<T>
return new ObjectEqualityComparer<T>();
}