Posted by pjc0247

[IL2CPP] Avoid Boxing

C#/Unity 2016.09.22 11:52

Avoid Boxing

IL2CPP OPTIMIZATION : Avoid Boxing 

Boxing은 코스트가 높은 연산이다. 이 글에서는 기존 C# 컴파일러는 특정한 상황에서 불필요한 Boxing이 수행되고, IL2CPP가 이를 어떻게 회피하는지를 보여준다.

기존 C# 컴파일러의 방식

interface HasSize {
   int CalculateSize();
}

struct Tree : HasSize {
   private int years;
   public Tree(int age) {
       years = age;
   }

   public int CalculateSize() {
       return years*3;
   }
}
public static int TotalSize<T>(params T[] things) where T : HasSize
{
   var total = 0;
   for (var i = 0; i < things.Length; ++i)
       if (things[i] != null)
           total += things[i].CalculateSize();
   return total;
}

위와 같은 C# 코드를 컴파일하면 아래와 같은 IL 코드가 생성된다. (Tree가 class가 아닌 struct라는 점에 주의)

// This is the start of the for loop

// Load the array
IL_0009: ldarg.0
// Load the current index
IL_000a: ldloc.1
// Load element at the current index
IL_000b: ldelem.any !!T
// What is this box call doing in here?!?
// (Hint: see the null check in the C# code)
IL_0010: box !!T
IL_0015: brfalse IL_002f

IL_0010 부분에서 box !!T명령어를 볼 수 있는데, null과 비교하려면 일단 레퍼런스 타입이어야 하기 때문에 값을 강제로 레퍼런스 타입으로 캐스팅하는작업이다.
T가 이미 레퍼런스 타입이라면 박싱은 아주 빠르게 끝나겠지만, T가 밸류 타입이라면 박싱 작업은 아래와 같은 단계를 거치게 된다.

  • 힙에 할당한다.
  • 가비지 컬렉터에 새 오브젝트가 생겼음을 알린다.
  • 밸류 타입 데이터를 할당된 공간에 옮긴다
  • 새 공간을 가리키는 레퍼런스 타입을 가져온다.

만약 TotalSize 메소드에 10000개짜리 배열을 넘겨주게 되면 위와 같은 작업이 10000번 일어난다는 뜻이다.
가장 어이없는건, 밸류 타입은 처음부터 null이 될수 없기 때문에 박싱의 코스트를 논하기 이전에 if (things[i] != null) 이 문장 자체가 아무런 의미가 없는 문장이 되어버린다. 결국 항상 true만을 리턴하는 조건식을 위해서 불필요한 박싱이 계속 일어나게 된다는 점이다. 

이는 C# 컴파일러가 C++와 같은 템플릿 방식이 아니라 제너릭방식을 채용했기 때문이다. TotalSize 메소드는 일단HasSize를 구현하기만 했으면 밸류 타입이던, 레퍼런스 타입이던 모두 넘어올 수 있다. 컴파일러는 어쩔수 없이 둘 다 대응하는 코드를 작성해야 한다. 

IL2CPP 에서는

원본 글에서는 이렇게 언급하고 있다. IL2CPP will create an implementation of The TotalSize<T> method specifically for the case where T is a Tree.
C#의 하나의 제너릭한 메소드만을 생성하는 방법 대신, 최적화를 위해서 예전의 C++ 시절 방법으로 회귀한다는 것이다. 주어진 T가 Tree처럼 밸류 타입이라면 해당 타입을 위한 메소드를 한벌 더 준비하게 된다. 

IL_0009:

// Load the array
TreeU5BU5D_t4162282477* L_0 = ___things0;
// Load the current index
int32_t L_1 = V_1;
NullCheck(L_0);
IL2CPP_ARRAY_BOUNDS_CHECK(L_0, L_1);
int32_t L_2 = L_1;
// Load the element at the current index
Tree_t1533456772  L_3 = (L_0)->GetAt(static_cast<il2cpp_array_size_t>(L_2));

// Look Ma, no box and no branch!

// Set up the arguments for the method and it call
int32_t L_4 = V_0;
TreeU5BU5D_t4162282477* L_5 = ___things0;
int32_t L_6 = V_1;
NullCheck(L_5);
IL2CPP_ARRAY_BOUNDS_CHECK(L_5, L_6);
int32_t L_7 = Tree_CalculateSize_m1657788316((Tree_t1533456772 *)(
                (L_5)->GetAddressAt(static_cast<il2cpp_array_size_t>(L_6))), /*hidden argument*/NULL);

// Do the next loop iteration...

실제로 원글의 최적화된 IL2CPP 코드 부분을 보면 박싱도 없고 null 체크도 완전히 생략된것을 볼 수 있다.

Posted by pjc0247
TAG C#, IL2CPP, Unity

IL2CPP는 유니티에서 발표한 AOT 컴파일러이다.


특이한점이 있다면, 인풋은 이미 빌드된 IL 코드 (DLL)이며, 아웃풋은 C++ 코드이다.

그러니까 이미 빌드된 바이너리를 다시 뜯어서 컴파일 가능한 C++ 소스코드로 만들어낸다는건데, 어떻게보면 AOT가이나리 디컴파일로 생각할수도 있겠다.



유니티가 주장하는 IL2CPP의 장점

- 퍼포먼스 : c++로 변환되서 네이티브 어셈블리로 실행되기 때문에 퍼포먼스 향상이 있다고 한다. 근데 처음부터 c++로 짜여진 코드도 아니고, IL을 c++ 코드로 변환한것이기때문에 최종 결과물인 c++ 코드에 .Net VM 호환성을 위한 코드들이 상당히 많이 들어간다. 어차피 Mono도 JIT이니, AOT니 전부 지원하는데 큰 효과가 있느지는 잘 모르겠다. 유니티측 벤치는 IL2CPP를 적용하면 빨라진다고 주장하지만 다른 유저가 측정한 벤치는 오히려 느려진다는 벤치도 있고, 스택오버플로우에는 IL2CPP로 변경했더니 게임 프레임이 심각하게 떨어졌다는 질문도 있는 상황이다. 애초에 IL2CPP 퍼포먼스 관련 글이 어엄청나게적음.

- 포터블(?) : 자바가 vm이 있는 구조를 내세우면서 c++과 비교해서 내세운 장점도 이거다. 근데 유니티는 반대의 주장을 하고있다. 아마도 플랫폼(OS)이 아니라 아키텍쳐에 대해서 이식성이 좋다는 소리를 하고싶다는것 같은데 자기들이 모든 아키텍쳐에 대해서 모노를 지원할 역량이 안되니 그냥 c++ 컴파일러에게 일을 미루겠다는뜻으로밖에 안보인다.

    


중간 언어와 비교해서의 단점

- 컴파일 시간이 길어진다. : 유니티에서 xcode 프로젝트로 출력하는 시간도 어마어마하고 / 엑스코드에서 앱으로 빌드하는시간도 어마어마해진다. C#의 장점이 컴파일이 빠르다는건데 유니티는 진짜 C#의 장점만 골라서 다버리는 삽질에는 따라올수없는 강자다.

- 버그가 너무 많이생긴다. : IL2CPP는 기존 코드에 대해서 투명성 있게 작동해야 하는게 정상이지만, 그런것치고는 버그가 어마어마하다. 나도 몇가지를 본적이 있는데 주로 리플렉션을 사용하는 JSON 라이브러리들에서 발생한다. 일단 이쪽에서 버그가 발생하면 뭐 손 쓸 수 있는 방도가 없다. 그냥 유니티에 버그리포트하고 어마어마하게 느린 응답을 기다리는것밖에는.
실제로 유니티 릴리즈 노트에 올라오는 패치노트중 IL2CPP가 차지하는 부분은 어마어마하다.

- .Net의 풀스펙을 사용할 수 없다. : 사실 이부분은 유니티가 모노 2.0을 사용하는 시점에서 기대도 안하기때문에 별로 길게 안씀



그럼 왜 만들었나

인터넷에 올라오는 글 중에는 종종 iOS 64비트 지원을 위해서 내놓은 솔루션이라고 하는 글이 있는데 그건 그냥 옛날부터  IL2CPP를 개발하고 있었지만, 마침 iOS 64비트 지원 의무화 이슈가 터지면서 덜만들어졌음에도 불구하고 iOS에 베타성격으로 먼저 풀어버린것 같다. 
실제로 IL2CPP는 iOS뿐만 아니라 지금 안드로이드나 윈도 스토어버전까지도 하나하나씩 나오고있는데, 이걸 봐서는 모노 런타임을 완전히 대체할 큰그림을 그리고있는것으로 생각된다. 지네도 2010년버전인 모노2.0들고 타이젠이니 윈폰이니 지원하기 힘드니까 그냥 일을 다 c++ 컴파일러에게 미뤄버리려는 속셈으로 생각되고 퍼포먼스는 그냥 거기서 딸려 나오는 보너스라고 생각.


* 닷넷도 그렇지만 프레임워크랑 런타임은 별개다. 모노 프레임워크를 교체한다는 뜻은 아님

Posted by pjc0247
TAG C#, IL2CPP, Unity


try~catch 로 처리되지 못하고 밖으로 흘러내린 익셉션을 잡아내는 법


유니티가 메인 스레드에서 발생하는 익셉션은 유니티 내부의 try~catch로 잡아내는 듯 하다.

그 결과 유니티 메인 스레드와, 외부 스레드의 익셉션을 모두 잡아내기 위해서는 위 코드와 같이 기이한 구조로 걸러내야 한다.


Posted by pjc0247
언어 코드 (language code) 가져오기


유니티에서 C#의 컬쳐 코드를 가져오는 API (CurrentCulture) 를 사용하면 무조건 "en-US" 고정값이 나온다.
https://feedback.unity3d.com/suggestions/fix-localization-issues-with-cor )


이를 해결하는 (아마 유일한) 방법은 각 OS별로 제공되는 네이티브 API를 호출하는 것 뿐이다.


* 언어 코드가 필요한 것이 아니라, 그냥 단순히 구분만 하고 싶다면 UnityEngine.Application.systemLanguage 를 사용하면 된다.


Posted by pjc0247
TAG C#, Unity

리플렉션 등을 이용할 때, 코드 스트리핑에 의해서 해당 생성자에 대해서 CPP 코드가 생성이 안된 경우
(리플렉션이 게임 코드에 없더라도, 게임 내부에서 사용하는 다른 라이브러리 코드에 있을 수 있다.)

유니티는 쓰지 않는다고 판단하는 코드는 자동으로 제거하기 때문에, 쓰는척을 해줘야한다.



아래 코드는 코드 스트리핑을 방지하기 위한 트릭
(아래 익셉션은 주로 json 라이브러리에서 일어난다.,)

MissingMethodException: Method not found: 'Default constructor not found...ctor() of System.ComponentModel.Int32Converter

MissingMethodException: Method not found: 'Default constructor not found...ctor() of System.ComponentModel.Int64Converter



Posted by pjc0247
TAG C#, Unity

IL2CPP 환경에서 Assembly.GetCallingAssembly().GetTypes() 메소드가 기대한 것과 같이 동작하지 않을 때


어셈블리 문제

알 수 없는 이유(버그?) 로 Assembly.GetCallingAssembly() 메소드는 Mono2x 일때와 IL2CPP 일 때 다른 값이 반환됩니다. 이를 해결하기 위해 해당 메소드 대신 typeof(FooType).Assembly 식의 접근법으로 어셈블리를 가져오는것으로 고칠 수 있습니다. 

(FooType은 찾고자 하는 어셈블리 안에 들어있는 타입이어야 합니다.)


(또는) 코드 스트리핑 

http://docs.unity3d.com/kr/current/Manual/iphone-playerSizeOptimization.html

스트립은 정적 코드 분석에 의존하고 있어 효율적으로 수행하기 어려운 경우가 있으며, 특히 reflection처럼 동적 기능이 사용되는 경우 그렇습니다. 그런 상황에서 어떤 클래스를 제외하거나 정보를 팁으로 전달해야 합니다. Unity는 블랙리스트 를 프로젝트 단위로 지원합니다. 블랙리스트를 사용하려면link.xml 파일을 만들어 Assets 폴더에 넣으면 됩니다. link.xml의 기재 방법의 예제는 다음과 같습니다 : -

유니티는 바이너리 최적화를 위해 쓰이지 않는 코드를 빌드타임에 제거합니다.

C#의 리플렉션 기능을 사용해서만 접근되는 클래스들(예를들어 자동 테스트되는 테스트 단위들)은 대부분 코드상에서 직접적으로 레퍼런싱 되지 않기 때문에 사용하지 않는 클래스라고 판단되어 제거됩니다. 이를 방지하기 위해서는 link.xml 파일을 작성해 삭제하지 않고 남겨야 하는 타입들을 지정합니다.


Posted by pjc0247
TAG IL2CPP, Unity

유니티의 안드로이드 빌드 환경을 설정에서 키스토어 파일을 등록할 때, 일반적으로 Browe Keystore 버튼으로 파일을 등록하게 되면 위 사진과 같이 절대경로가 설정된다.


자동 빌드 환경이나, 서로 다른 빌드 머신에서 빌드하고자 할 때 굉장히 짜증나는 부분이며 제대로된 해결 방법은 없고 편법을 사용해야 하는 것을 보인다.


유니티 상단 메뉴 -> Edit -> Project Settings -> Editor -> Asset Serialization

값을 Force Text 로 변경한다.

이 작업은 다른 설정 또는 .unity 파일도 모두 변경합니다.


프로젝트 경로 -> ProjectSettings 폴더 -> ProjectSettings.asset 을 열면 텍스트로 변환되어 있다. 이중 AndroidKeystoreName 을 검색하여 절대 경로를 상대 경로로 변경한다.


유니티를 다시 실행시켜 결과를 확인한다.



빌드 머신에서 실행해 결과를 확인한다.



Posted by pjc0247

Unity 5.3 버전에서의 변경 사항 중에는 JSON API의 추가가 포함되어 있습니다. (http://blogs.unity3d.com/kr/2015/12/08/unity-5-3-all-new-features-and-more-platforms/)

이전 버전의 유니티에서는 JSON을 사용하려면 외부 라이브러리를 사용하여야 했는데,
각각의 JSON 라이브러리는 iOS/IL2CPP와 호환이 좋지 않은 것들도 있었으며, 게임에 추가한 외부 플러그인에서 JSON 라이브러리를 사용하는 경우도 있어 실제 게임 앱은 하나인데 그 안에 JSON 라이브러리만 2~3종류가 들어가있는 경우도 종종 있었습니다. 

유니티에서는 아마 이러한 문제점들을 파악하고 이를 위한 해결책으로 Unity 자체적으로 JSON 라이브러리를 제공하게 된 것 같습니다.


유니티의 새로운 API인 Json Utility는 매우 쓰기 쉽고 기본적인 기능만 제공하지만 게임에서 쓰기에는 기능이 충분합니다. 여기서는 간단한 JsonUtility의 사용법들을 알아보도록 하겠습니다.

  
기본적인 데이터를 Json 으로 변경하는 코드입니다.
이렇게 게임에서 사용하는 세이브 데이터 등을 Json으로 저장할 수 있습니다.
주의할점은 public field만 변환됩니다. public property는 변환되지 않습니다.

  
반대로 Json으로부터 오브젝트를 만드는 코드입니다.
제너릭으로 오브젝트의 타입을 지정합니다. Dictionary<string, object> 타입은 동작하지 않는 것 처럼 보입니다.

  
Json으로부터 이미 있는 오브젝트를 업데이트하는 코드입니다. 레벨이 원래 5인 오브젝트를, FromJsonOverride 메소드를 이용해 레벨을 99로 Override 합니다.


Posted by pjc0247