Log Stash

as an Industrial Personnel

Note

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

SavvyTuna 2019. 12. 23. 23:39

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

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

C#

v.3.5

일단 닷넷 버전 3.5에서 진짜 boxing을 하는지 IL만 잠깐 살펴보도록 한다.

테스트 코드는 아래처럼 작성했는데, 아무래도 상관 없다. 어차피 mscorlib 안쪽만 볼 예정이다.

class Program
{
    enum Kind
    {
        Morty,
        Rick,
    }

    class Entity 
    {
        public string Name = "";
    }

    static void DictTest()
    {
        var dict = new Dictionary<Kind, Entity>
        {
            { Kind.Morty, new Entity() { Name = "Morty" } },
            { Kind.Rick, new Entity() { Name = "Rick" } }
        };

        var name = dict[Kind.Morty].Name;
        Console.WriteLine(name);
    }

    static void Main(string[] args)
    {
        Console.WriteLine("Hello world");

        DictTest();
    }
}

아래 캡쳐는 dnSpy로 본 mscorlib dll에 있는 IL의 C# version view다. 툴 내부에 있는 디버깅 기능을 사용해서 로직 진행을 타고 들어가 봤다.

Dictionary를 생성 할 때 별 다른 비교자 객체를 주입하지 않으면, 전에 말한대로 Default 프로퍼티를 타고 들어가 CreateComparer()를 부르고, 내부 if statement들을 계속 지나치다 마지막으로 ObjectEqualityComparer를 생성하게 된다.

그렇게 생성한 Dictionary에서 key값으로 Element를 얻어오려고 하면, 아래 캡쳐 하단 부분에 있는 것 처럼 Call stack을 타면서 Equality를 비교한다.

Equals() 메소드 내부의 IL을 보면 'box'만 4번 불린다. 자세한 프로파일링은 하지 않았지만 성능에 좋아보이진 않음.

v.4.6

visual studio project property 설정 화면에서 닷넷 프레임워크 버전을 4.6으로 맞춰놓고 코드는 그대로 냅둔 채로 다시 빌드를 걸어 본다.

마찬가지로 Debug 기능으로 타고 들어가, Step into 기능으로 비교자 객체를 생성하는 부분까지 따라 들어가면 아래와 같은 루틴을 타게 된다. 위에서 링크 걸어놓은 CreateComparer() 루틴과 같고, 실제로 로직을 따라가보면 Enum 여부를 판단해서 EnumEqualityComparer<T>를 생성한다.

EnumEqualityComparer<T>Equals() 메소드 내부 IL. 일단은 'Box'가 하나도 보이지 않는다. 단순 method call, evaluation stack store/load instructions 밖에 없다.

Unity (windows)

유니티에서 차이를 볼 수 있는지 확인해보자. 안드로이드 iOS빌드는 귀찮아서 안 해봤고 일단 윈도우에서만 돌려 봤다.

매 프레임당 만번씩 Dictionary element에 접근하는 예시 코드를 작성한다. 얘를 만만한 아무 GameObject에 붙여서 실행되게 만들자.

public class DictBoxTest : MonoBehaviour
{
    enum Kind
    {
        Morty,
        Rick,
    }

    class Entity
    {
        public string Name = "";
    }

    Dictionary<Kind, Entity> dict = new Dictionary<Kind, Entity>
    {
        { Kind.Morty, new Entity() { Name = "Morty" } },
        { Kind.Rick, new Entity() { Name = "Rick" } }
    };

    void Update()
    {
        // ask name 10000 times per frame
        for (int i = 0; i < 10000; i++)
        {
            var name = dict[Kind.Morty].Name;
        }
    }
}

.NET 3.5 Equivalent

아래처럼 Script Backend를 설정하고, 유니티 프로파일러를 돌려봤다.

매 프레임마다 쓰레기를 만들어대니 일정한 속도로 'Total GC Allocated'가 상승하고, GC.Collect()가 불린 이후로 잠시 낮아졌다가 다시 올라가는 상태를 반복한다.

GC Alloc이 일어나는 장소는 Dictionary의 get_item (내부의 Equals()).

.Net 4.X Equivalent

아래처럼 Script Backend 설정을 수정하고, 스크립트를 refresh, 다시 빌드한다.

적어도 Dictionary의 get_item으로 만들어지는 쓰레기는 보이지 않는다. 일단은 문제 없어 보인다.

위에서 언급된 예시 코드들은 별 생각 없이 작성했다. 뭔가 아닌것 같다면 제보 바람.