Create a gist now

Instantly share code, notes, and snippets.

Embed
What would you like to do?

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 체크도 완전히 생략된것을 볼 수 있다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment