• [.NET / C#] Generic Collection에서 enum의 사용은 boxing을 발생 시키는가?

    2021. 5. 6.

    by. xxx123

    서론

    NDC2019 자료중 흥미로운 내용을 발견했는데...

    http://ndcreplay.nexon.com/NDC2019/sessions/NDC2019_0040.html#k%5B%5D=enum

    강연자는 Generic Collection에서 enum을 사용하면 boxing이 일어날 가능성이 높다고 말하고 있다.

    요약하자면 내용은 다음과 같다.

     

    • Generic Collection의 함수 중 equals()를 내부적으로 호출하는 경우에 값 타입인 enum을 참조 타입인 object 타입으로 변환하기 때문에 boxing이 발생한다.
    • ex)
      • Dictionary<K, V>의 Key가 Enum이고, ContainKey() 함수를 사용 경우.
      • List<T>의 T가 Enum이고, Contains(), Remove(), IndexOf() 함수를 호출 한 경우.

     

     

    실제로 그런지가 궁금하여 테스트를 진행해 보았다.

    테스트는 정성태님이 GC가 발생하는지를 체크하실 때 사용 하는 코드를 조금 수정해서 진행해 보았다.

    using System;
    using System.Collections.Generic;
    using System.Threading;
    
    namespace ConsoleApp1
    {
    
        class Program
        {
            static void Main(string[] args)
            {
                Thread thread = new Thread(threadFunc);
                thread.IsBackground = true;
                thread.Start();
        
                while (true)
                {
                    int n = GC.CollectionCount(0) + GC.CollectionCount(1) + GC.CollectionCount(2);
                    Console.WriteLine(n);
                    Thread.Sleep(1000);
                }
            }
    
            enum TestEnum
            {
                enumValue1,
                enumValue2,
                enumValue3,
            }
    
            private static void threadFunc()
            {
                List<TestEnum> list = new List<TestEnum>();
                list.Add(TestEnum.enumValue1);
                list.Add(TestEnum.enumValue2);
                list.Add(TestEnum.enumValue3);
    
                while (true) // 반복해서 Contains 실행
                {
                    bool result = list.Contains(TestEnum.enumValue1);
                }
            }
        }
    }
    
    결과
    ...
    0
    0
    0

     

    아무리 시간이 지나도 GC는 발생하지 않는다.

    즉, boxing이 발생하지 않고 있다는 말이다.

    왜 그런것일까?

     

     

     

     

     

    .NET 4.0 이후 Generic Collection에서의 Enum 처리

    .NET 4.0 이후부터는 해당 문제가 수정 되었다.

    Dictionary<K,T>나 List<T>같은 Generic Collection들은 Equals를 사용할 일이 생기면 T 타입에 대한 Default Comparer를 생성하고, 이 Comparer의 Equals()를 호출하는 식으로 값을 비교해 나가는데.

    예전에는 Enum 타입이 T로 들어오면 이게 어떤 타입인지를 체크하지 못해서 object의 Equals()를 호출해 줬던 반면,

    .NET 4.0 이후 버전에서는 Enum 타입에 대한 체크를 수행하고 EnumEqualityComparer를 생성하여 Equals()를 호출하게 된다.

     

    다음은 List<T>의 Contains() 함수이다.

    public bool Contains(T item) { 
        if ((Object) item == null) { 
            for(int i=0; i<_size; i++) 
                if ((Object) _items[i] == null) 
                    return true; 
             return false; 
         } 
         else { 
              EqualityComparer<T> c = EqualityComparer<T>.Default; // Default의 호출. 기본 Comparer를 생성. 
              for(int i=0; i<_size; i++) { 
                  if (c.Equals(_items[i], item)) return true; 
              } 
              return false; 
         } 
    }
    

     

    EqualityComparer<T>의 Default를 호출하는 순간 T에 대한 Comparer를 생성하기 위해 CreateComparer()가 호출되게 된다.

    private static EqualityComparer<T> CreateComparer()
    {
    
    .....생략
                
    
       if (t.IsEnum) {   // T가 Enum 타입인가? 
           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); 
    		  // EnumEqualityComparer을 생성 
              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); 
               } 
        }
    
    

     

    T가 Enum 타입이라면 TypeCode에 맞는 EnumEqualityComparer를 생성하고 Equals()를 호출해주게 된다.

    internal class EnumEqualityComparer<T> : EqualityComparer<T>, ISerializable where T : struct
    {
        [Pure] 
        public override bool Equals(T x, T y) { 
    
           int x_final = System.Runtime.CompilerServices.JitHelpers.UnsafeEnumCast(x);
           int y_final = System.Runtime.CompilerServices.JitHelpers.UnsafeEnumCast(y);
    
           return x_final == y_final;
        }
    
    .....생략

    Equals()가 호출되게 되면 T는 int로 캐스팅되게 되고, 결국 int끼리의 값비교가 진행되게 된다.

    UnsafeEnumCast()의 구현은 공개되어 있지 않아서 실제로 어떻게 동작하는지 파악하긴 힘들지만 잘 캐스팅 해주겠지 ^^;

     

     

     

     

     

    정리

    .NET 4.0 이후의 Generic Collection에서는 Equals() 호출 시 boxing이 발생하지 않는다.

    이제는 Enum을 Generic Collection에서 사용하기 위해 별도의 Comparer을 만들어서 제공해주는 번거로움이 없을것 같다.

     

     

     

     

    [참고]


    http://ndcreplay.nexon.com/NDC2019/sessions/NDC2019_0040.html#k%5B%5D=enum


    // 이미 Unity에서도 테스트를 해본 분이 계셨다.
    enghqii.tistory.com/69

     

     

    댓글