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 체크도 완전히 생략된것을 볼 수 있다.
'C# > Unity' 카테고리의 다른 글
WWW로 렉없이 텍스쳐 로드하기 (0) | 2018.02.11 |
---|---|
[Unity] 왜 만들었는지 모를 IL2CPP (2) | 2016.06.22 |
[Unity] Unhandled Exception 처리하기 (0) | 2016.06.15 |
[Unity] ISO LanguageCode 가져오기 (0) | 2016.06.09 |
[Unity] IL2CPP 에서 MissingMethodException 가 발생할 때 (0) | 2016.04.14 |