Nexon Korea
이무림
편리하고 성능좋게 Enum 사용하기
Enum의 Boxing을 어찌할꼬?
발표자소개
⚫ 넥슨 데브캣 스튜디오 MT팀
⚫ <런웨이 스토리>
⚫ <로드러너 1>
⚫ <링토스 세계여행>
⚫ <마비노기 2>
⚫ <마비노기 360>
⚫ <루니아 전기>
이무림
목차
⚫ Enum 과 Boxing
⚫ 정석적 해법
⚫ Generic Enum 과 Int 사이의 변환
▪ 동적 코드 생성
▪ C++ Union 트릭
▪ Unsafe Pointer 트릭
⚫ EnumDictionary
Enum 과 Boxing
Boxing 이란?
⚫ 값 타입을 참조 타입으로 변환
▪ ex) int -> object
⚫ 값 타입은 Stack 메모리에 위치
⚫ 참조 타입은 Heap 메모리에 위치
⚫ Heap 에 공간을 할당해서(Box)
Stack 에 있는 값을 복사해 넣음
Boxing 예제
Boxing의 뒷처리
⚫ 1회성으로 생성된 수많은 Box는
Gabage로 변한다.
⚫ Gabage Collection 이 자주
필요하게 됨
⚫ GC가 발생하면 게임이 멈칫.
Enum과 Generic의 만남
⚫ Enum 타입을 Generic으로 받으면 비교할 때 Boxing을 피할 수가 없다.
Enum과 Generic의 만남
⚫ System.Object.Equals() 메소드가 사용되기 때문
▪ object로 인자를 전달받기 위해 오른쪽 객체가 Boxing된다.
▪ virtual 메소드이므로 왼쪽 인자가 참조객체로 Boxing된다.
Enum과 Generic의 만남
⚫그런 코드 사용 안하면 되지!
⚫그런데 정말 사용하지 않을 수 있을까?
Enum과 Generic의 만남
⚫Dictionary<K, V> 에 Enum Key가 들어간다면?
▪ 설마… MS가 이런 것 하나 알아서 안 해주겠어?
⚫설마설마 하던 그 설마가…… Boxing 발생.
List<T> 너마저도?
⚫ 기본중에서도 기본 컨테이너 List<T>는 Enum 을 잘 다룰 수 있을까?
⚫ Contains(), Remove(), IndexOf()처럼
내부적으로 Equals()를 호출하는 경우 동일하게 Boxing 발생
Enum.HasFlag()
⚫ HasFlag() 메소드는 편리한 Bitmasking 처리를 해준다.
▪ .Net 4.5 에서 사용가능
Enum.HasFlag()
⚫ HasFlag() 함수의 내부를 살펴보자.
⚫ Boxing 이 발생한다.
Box의 크기
⚫ Boxing이 발생하면 메모리를 얼마나 차지할까?
⚫ 값 타입의 최소 크기는 4 byte
⚫ 참조 타입의 최소 크기 12 byte
▪ 32bit System 기준
⚫ Equals() 호출시 Boxing 2번 발생
⚫ 총 24 Byte 사용
⚫ 한 프레임에 40번 비교하면 24 x 40 ~= 1 Kbyte Gabage 발생
정리: Enum과 Boxing
⚫Boxing은 성능에 악영향을 미친다.
최대한 줄여야 한다.
⚫Enum과 Generic이 만나면,
비교과정에서 Boxing이 발생한다.
정석적 해법
정석적 해법
⚫ 표준 컨테이너는 생성자에서 Comparer 를 전달받을 수 있다.
정석적 해법
⚫ Enum 타입 하나당 Comparer 타입 하나를 구현해야 한다.
⚫ 컨테이너를 생성할 때마다 Comparer 를 전달해야 한다.
⚫극한직업: 프로그래머 편
⚫ 이것은 해법인가? 노가다인가?
C# 최신 버전이라면?
⚫ C# 7.3 부터 Generic Contstraint 로 Enum 을 지정가능
⚫ T 가 Enum 인 것을 알고 있으니 Equals()를 최적화 해주겠지?
C# 최신 버전이라면?
⚫ IL 을 확인해보자. (Release 빌드)
▪ box 명령어에서 한번
▪ constrained.callvirt 내부에서 box 발생
정리: 정석적 해법
⚫정석적인 방법은 너무 번거롭고 손이 많이 간다.
⚫최신 스펙의 Enum Generic Constraint 를 이용해도
Boxing이 발생한다.
⚫다른 방법이 없을까?
Generic Enum 과 Int 사이의 변환
Generic Enum 과 Int 사이의 변환
⚫ Enum은 그저 숫자에 불과하다.
▪ BackField 타입을 지정할 수 있다.
▪ 아무것도 없으면 int
⚫ Int 로 변환할 수 있다면 Boxing 없이 비교할 수 있다.
▪ 일반화된 Bitmasking처리도 가능하다.
Generic Enum 과 Int 사이의 변환
⚫ 개별적인 Enum타입은 쉽게 int 로 변환할 수 있다.
⚫ Generic 으로 받은 Enum은 변환이 어렵다.
⚫ 방법을 찾아보자.
동적 코드생성
동적 코드생성
⚫ 개별적인 Enum타입은 쉽게 int 로 변환할 수 있다.
⚫ Generic 으로 받은 Enum은 변환이 어렵다.
⚫ Generic 타입의 구체타입이 무엇이 될것인지
컴파일 시점에서는 확정할 수 없기 때문.
⚫ 프로그램이 실행되는 중에는 구체적인 타입이 무엇인지 알고 있다.
⚫ 즉시 코드를 생성해서 변환한다.
동적 코드생성
⚫ 런타임에 개별 enum 타입을 캐스팅하는 코드를 생성한다.
* ValueCastTo<TTo> 클래스로 분리하지 않으면 유니티 구버전에서는 컴파일 에러가 발생한다.
동적 코드생성
⚫ 사용법
⚫ 동적 코드 생성이라니, 기술 그 자체로 멋있다!
⚫ 문제 해결!
⚫ 하지만 AOT(Ahead Of Time) 플랫폼에서 동작하지 않는다.
▪ 망해라 애플
C++ Union 트릭
C++ Union 트릭
⚫ C#의 조상언어인 C++에는 Union 타입이 존재한다.
⚫ Union에 선언된 필드는 같은 메모리를 액세스한다.
⚫ C#은 Union 타입을 지원하지 않지만
[StructLayout], [FieldOffset] 특성을 사용하여 흉내낼 수 있다.
C++ Union 트릭
⚫ C++ Union 스타일로 필드를 겹치게 배치해서 읽고 쓴다.
C++ Union 트릭
⚫ 잘 동작한다!
⚫ 성능도 너무 좋다!
⚫ 그런데 강타입 언어가 이렇게 쉽게 타입체크를 우회해도 되나?
C++ Union 트릭
⚫ 런타임 구현에 따라 다르다.
⚫ .Net 3.5 Equivalent 에서는 잘 동작
▪ 내부적으로 Mono 2.6.5 런타임 사용
⚫ .Net 4.x Equivalent에서는 부작용 존재
▪ 내부적으로 Mono 4.2.2 런타임 사용
⚫ 이 기법을 적용한 Assembly에 Reflection을 시도하면 예외가 발생한다.
▪ System.TypeLoadException: Generic class cannot have explicit layout.
Unsafe Pointer 트릭
Unsafe Pointer 트릭
⚫ C#에서도 C++처럼 포인터를 사용할 수 있다.
⚫ 단, unsafe 블록 안에서만 가능.
⚫ int 형 포인터를 이용해서 Enum 위치를 읽어들이자.
Unsafe Pointer 트릭
⚫ 포인터를 이용해서 Enum 필드에 접근한다.
Unsafe Pointer 트릭
⚫ 잘 동작한다.
⚫ 컴파일시 -unsafe 스위치가 있어야 컴파일이 가능하다.
⚫ Unity2017.x 에서는 unsafe 가 디폴트
⚫ Unity2018.x 에서는 설정에서 선택가능
* 유니티 구버전에서 unsafe 플래그를 지정하려면 Assets 폴더에 mcs.rsp, csc.rsp 파일을 생성하고 내용에 -unsafe 를 추가한다.
EnumDictionary
EnumDictionary
⚫ Enum을 Key로 사용해도 Boxing이 발생하지 않는 Dictionary
⚫ Enum을 내부에서 int 로 변환해서 저장하고
내보낼 때 다시 Enum타입으로 변환한다.
EnumDictionary
⚫ 구현
EnumDictionary
⚫ #if 를 사용해서 플랫폼에 따라 구현을 선택한다.
⚫ 기본적으로 동적 코드생성을 사용
⚫ Unity 환경에서는
⚫ .Net 3.5 에서는 C++ Union 트릭사용
⚫ .Net 4.x 에서는 Unsafe Pointer 트릭사용
▪ 2018.x 버전에서는 allow unsafe code 옵션을 켜고 쓴다.
EnumDictionary
⚫ 사용법
⚫ 너무너무 편리하다.
EnumDictionary
⚫ 전체 코드
▪ https://github.com/netics01/EnumDictionary
⚫ BitConvert
▪ 모든 Enum 과 Int 변환방식 구현
⚫ EnumDictionary
⚫ EnumHashSet
⚫ 데브캣에서 사용하는 SUF의 일부
▪ Silvervine Unity Framework
* 64bit BackField 를 사용하는 Enum 에는 사용할 수 없다.
감사합니다.
이무림, Enum의 Boxing을 어찌할꼬? 편리하고 성능좋게 Enum 사용하기, NDC2019

이무림, Enum의 Boxing을 어찌할꼬? 편리하고 성능좋게 Enum 사용하기, NDC2019

  • 1.
    Nexon Korea 이무림 편리하고 성능좋게Enum 사용하기 Enum의 Boxing을 어찌할꼬?
  • 2.
    발표자소개 ⚫ 넥슨 데브캣스튜디오 MT팀 ⚫ <런웨이 스토리> ⚫ <로드러너 1> ⚫ <링토스 세계여행> ⚫ <마비노기 2> ⚫ <마비노기 360> ⚫ <루니아 전기> 이무림
  • 3.
    목차 ⚫ Enum 과Boxing ⚫ 정석적 해법 ⚫ Generic Enum 과 Int 사이의 변환 ▪ 동적 코드 생성 ▪ C++ Union 트릭 ▪ Unsafe Pointer 트릭 ⚫ EnumDictionary
  • 4.
  • 5.
    Boxing 이란? ⚫ 값타입을 참조 타입으로 변환 ▪ ex) int -> object ⚫ 값 타입은 Stack 메모리에 위치 ⚫ 참조 타입은 Heap 메모리에 위치 ⚫ Heap 에 공간을 할당해서(Box) Stack 에 있는 값을 복사해 넣음
  • 6.
  • 7.
    Boxing의 뒷처리 ⚫ 1회성으로생성된 수많은 Box는 Gabage로 변한다. ⚫ Gabage Collection 이 자주 필요하게 됨 ⚫ GC가 발생하면 게임이 멈칫.
  • 8.
    Enum과 Generic의 만남 ⚫Enum 타입을 Generic으로 받으면 비교할 때 Boxing을 피할 수가 없다.
  • 9.
    Enum과 Generic의 만남 ⚫System.Object.Equals() 메소드가 사용되기 때문 ▪ object로 인자를 전달받기 위해 오른쪽 객체가 Boxing된다. ▪ virtual 메소드이므로 왼쪽 인자가 참조객체로 Boxing된다.
  • 10.
    Enum과 Generic의 만남 ⚫그런코드 사용 안하면 되지! ⚫그런데 정말 사용하지 않을 수 있을까?
  • 11.
    Enum과 Generic의 만남 ⚫Dictionary<K,V> 에 Enum Key가 들어간다면? ▪ 설마… MS가 이런 것 하나 알아서 안 해주겠어? ⚫설마설마 하던 그 설마가…… Boxing 발생.
  • 12.
    List<T> 너마저도? ⚫ 기본중에서도기본 컨테이너 List<T>는 Enum 을 잘 다룰 수 있을까? ⚫ Contains(), Remove(), IndexOf()처럼 내부적으로 Equals()를 호출하는 경우 동일하게 Boxing 발생
  • 13.
    Enum.HasFlag() ⚫ HasFlag() 메소드는편리한 Bitmasking 처리를 해준다. ▪ .Net 4.5 에서 사용가능
  • 14.
    Enum.HasFlag() ⚫ HasFlag() 함수의내부를 살펴보자. ⚫ Boxing 이 발생한다.
  • 15.
    Box의 크기 ⚫ Boxing이발생하면 메모리를 얼마나 차지할까? ⚫ 값 타입의 최소 크기는 4 byte ⚫ 참조 타입의 최소 크기 12 byte ▪ 32bit System 기준 ⚫ Equals() 호출시 Boxing 2번 발생 ⚫ 총 24 Byte 사용 ⚫ 한 프레임에 40번 비교하면 24 x 40 ~= 1 Kbyte Gabage 발생
  • 16.
    정리: Enum과 Boxing ⚫Boxing은성능에 악영향을 미친다. 최대한 줄여야 한다. ⚫Enum과 Generic이 만나면, 비교과정에서 Boxing이 발생한다.
  • 17.
  • 18.
    정석적 해법 ⚫ 표준컨테이너는 생성자에서 Comparer 를 전달받을 수 있다.
  • 19.
    정석적 해법 ⚫ Enum타입 하나당 Comparer 타입 하나를 구현해야 한다. ⚫ 컨테이너를 생성할 때마다 Comparer 를 전달해야 한다. ⚫극한직업: 프로그래머 편 ⚫ 이것은 해법인가? 노가다인가?
  • 20.
    C# 최신 버전이라면? ⚫C# 7.3 부터 Generic Contstraint 로 Enum 을 지정가능 ⚫ T 가 Enum 인 것을 알고 있으니 Equals()를 최적화 해주겠지?
  • 21.
    C# 최신 버전이라면? ⚫IL 을 확인해보자. (Release 빌드) ▪ box 명령어에서 한번 ▪ constrained.callvirt 내부에서 box 발생
  • 22.
    정리: 정석적 해법 ⚫정석적인방법은 너무 번거롭고 손이 많이 간다. ⚫최신 스펙의 Enum Generic Constraint 를 이용해도 Boxing이 발생한다. ⚫다른 방법이 없을까?
  • 23.
    Generic Enum 과Int 사이의 변환
  • 24.
    Generic Enum 과Int 사이의 변환 ⚫ Enum은 그저 숫자에 불과하다. ▪ BackField 타입을 지정할 수 있다. ▪ 아무것도 없으면 int ⚫ Int 로 변환할 수 있다면 Boxing 없이 비교할 수 있다. ▪ 일반화된 Bitmasking처리도 가능하다.
  • 25.
    Generic Enum 과Int 사이의 변환 ⚫ 개별적인 Enum타입은 쉽게 int 로 변환할 수 있다. ⚫ Generic 으로 받은 Enum은 변환이 어렵다. ⚫ 방법을 찾아보자.
  • 26.
  • 27.
    동적 코드생성 ⚫ 개별적인Enum타입은 쉽게 int 로 변환할 수 있다. ⚫ Generic 으로 받은 Enum은 변환이 어렵다. ⚫ Generic 타입의 구체타입이 무엇이 될것인지 컴파일 시점에서는 확정할 수 없기 때문. ⚫ 프로그램이 실행되는 중에는 구체적인 타입이 무엇인지 알고 있다. ⚫ 즉시 코드를 생성해서 변환한다.
  • 28.
    동적 코드생성 ⚫ 런타임에개별 enum 타입을 캐스팅하는 코드를 생성한다. * ValueCastTo<TTo> 클래스로 분리하지 않으면 유니티 구버전에서는 컴파일 에러가 발생한다.
  • 29.
    동적 코드생성 ⚫ 사용법 ⚫동적 코드 생성이라니, 기술 그 자체로 멋있다! ⚫ 문제 해결! ⚫ 하지만 AOT(Ahead Of Time) 플랫폼에서 동작하지 않는다. ▪ 망해라 애플
  • 30.
  • 31.
    C++ Union 트릭 ⚫C#의 조상언어인 C++에는 Union 타입이 존재한다. ⚫ Union에 선언된 필드는 같은 메모리를 액세스한다. ⚫ C#은 Union 타입을 지원하지 않지만 [StructLayout], [FieldOffset] 특성을 사용하여 흉내낼 수 있다.
  • 32.
    C++ Union 트릭 ⚫C++ Union 스타일로 필드를 겹치게 배치해서 읽고 쓴다.
  • 33.
    C++ Union 트릭 ⚫잘 동작한다! ⚫ 성능도 너무 좋다! ⚫ 그런데 강타입 언어가 이렇게 쉽게 타입체크를 우회해도 되나?
  • 34.
    C++ Union 트릭 ⚫런타임 구현에 따라 다르다. ⚫ .Net 3.5 Equivalent 에서는 잘 동작 ▪ 내부적으로 Mono 2.6.5 런타임 사용 ⚫ .Net 4.x Equivalent에서는 부작용 존재 ▪ 내부적으로 Mono 4.2.2 런타임 사용 ⚫ 이 기법을 적용한 Assembly에 Reflection을 시도하면 예외가 발생한다. ▪ System.TypeLoadException: Generic class cannot have explicit layout.
  • 35.
  • 36.
    Unsafe Pointer 트릭 ⚫C#에서도 C++처럼 포인터를 사용할 수 있다. ⚫ 단, unsafe 블록 안에서만 가능. ⚫ int 형 포인터를 이용해서 Enum 위치를 읽어들이자.
  • 37.
    Unsafe Pointer 트릭 ⚫포인터를 이용해서 Enum 필드에 접근한다.
  • 38.
    Unsafe Pointer 트릭 ⚫잘 동작한다. ⚫ 컴파일시 -unsafe 스위치가 있어야 컴파일이 가능하다. ⚫ Unity2017.x 에서는 unsafe 가 디폴트 ⚫ Unity2018.x 에서는 설정에서 선택가능 * 유니티 구버전에서 unsafe 플래그를 지정하려면 Assets 폴더에 mcs.rsp, csc.rsp 파일을 생성하고 내용에 -unsafe 를 추가한다.
  • 39.
  • 40.
    EnumDictionary ⚫ Enum을 Key로사용해도 Boxing이 발생하지 않는 Dictionary ⚫ Enum을 내부에서 int 로 변환해서 저장하고 내보낼 때 다시 Enum타입으로 변환한다.
  • 41.
  • 42.
    EnumDictionary ⚫ #if 를사용해서 플랫폼에 따라 구현을 선택한다. ⚫ 기본적으로 동적 코드생성을 사용 ⚫ Unity 환경에서는 ⚫ .Net 3.5 에서는 C++ Union 트릭사용 ⚫ .Net 4.x 에서는 Unsafe Pointer 트릭사용 ▪ 2018.x 버전에서는 allow unsafe code 옵션을 켜고 쓴다.
  • 43.
  • 44.
    EnumDictionary ⚫ 전체 코드 ▪https://github.com/netics01/EnumDictionary ⚫ BitConvert ▪ 모든 Enum 과 Int 변환방식 구현 ⚫ EnumDictionary ⚫ EnumHashSet ⚫ 데브캣에서 사용하는 SUF의 일부 ▪ Silvervine Unity Framework * 64bit BackField 를 사용하는 Enum 에는 사용할 수 없다.
  • 45.