[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

원본 : https://github.com/pjc0247/LinqExpressionSnippets


LinqExpressionSnippets

System.Linq.Expression 네임스페이스 아래의 기능들에 대한 복붙용 코드 조각들.

Lambda를 Expression으로 전달받기

람다의 타입 판별

public static void Foo<T>(Expression<Func<T>> f)
{
    // Foo(() => Math.Abs(1));
    if (f.Body is MethodCallExpression)
        Console.WriteLine("MethodCallExpression");

    // Foo(() => pos.x);
    if (f.Body is MemberExpression)
        Console.WriteLine("MemberExpression");

    // Foo(() => 10);
    if (f.Body is ConstantExpression)
        Console.WriteLine("ConstExpression");

    // Foo(() => a > b);
    if (f.Body is BinaryExpression)
        Console.WriteLine("BinaryExpression");
}

MethodCallExpression

// Foo(() => Math.Abs(1));
if (f.Body is MethodCallExpression)
{
    var body = f.Body as MethodCallExpression;

    // System.Math
    Console.WriteLine(body.Method.DeclaringType);

    // Abs
    Console.WriteLine(body.Method.Name);

    // 1
    Console.WriteLine(body.Arguments[0]);
}

MemberExpression

// Foo(() => pos.x);
if (f.Body is MemberExpression)
{
    var body = f.Body as MemberExpression;

    // Vector2
    Console.WriteLine(body.Member.DeclaringType);

    var fieldExp = (MemberExpression)body.Expression;
    // pos
    Console.WriteLine(fieldExp.Member.Name);

    // System.Int32
    Console.WriteLine(body.Type);

    // x
    Console.WriteLine(body.Member.Name);
}

ConstantExpression

// Foo(() => 10);
if (f.Body is ConstantExpression)
{
    var body = f.Body as ConstantExpression;

    // System.Int32
    Console.WriteLine(body.Type);

    // 10
    Console.WriteLine(body.Value);
}

BinaryExpression

// Foo(() => a > b);
if (f.Body is BinaryExpression)
{
    var body = f.Body as BinaryExpression;

    // Expression
    Console.WriteLine(body.Left);
    // Expression
    Console.WriteLine(body.Right);

    // ExpressionType.GreaterThan
    Console.WriteLine(body.NodeType);
}

Exression 트리 생성하기

Hello World

var exp = Expression.Block(
    Expression.Call(
        null,
        typeof(Console).GetMethod("WriteLine", new Type[] { typeof(string) }),
        Expression.Constant("Hello World")
    ));

var lambda = Expression.Lambda<Action>(exp);
lambda.Compile()();

local variables

var loc1 = Expression.Variable(typeof(int));
var loc2 = Expression.Variable(typeof(int));

var exp = Expression.Block(
    // 지역 변수 선언
    new ParameterExpression[] {
        loc1, loc2
    },

    // 메소드 바디
    Expression.Assign(
        loc1,
        Expression.Constant(10)),
    Expression.Call(
        null,
        typeof(Console).GetMethod("WriteLine", new Type[] { typeof(int) }),
        loc1
    ));

var lambda = Expression.Lambda<Action>(exp);
lambda.Compile()();

If ~ Else

var loc1 = Expression.Variable(typeof(int));
var loc2 = Expression.Variable(typeof(int));

var exp = Expression.Block(
    // 지역 변수 선언
    new ParameterExpression[] {
        loc1, loc2
    },

    // 메소드 바디
    Expression.Assign(
        loc1,
        Expression.Constant(11)),

    Expression.IfThenElse(
        Expression.Equal(loc1, Expression.Constant(10)),

        Expression.Call(
            null,
            typeof(Console).GetMethod("WriteLine", new Type[] { typeof(string) }),
            Expression.Constant("loc1 is 10")),

        Expression.Call(
            null,
            typeof(Console).GetMethod("WriteLine", new Type[] { typeof(string) }),
            Expression.Constant("loc1 is not 10"))
    ));

var lambda = Expression.Lambda<Action>(exp);
lambda.Compile()();

트리 끼워넣기(Injection)

Before, After

static void Zoo(Expression<Action> f)
{
    var exp = Expression.Block(
        Expression.Call(
            null,
            typeof(Console).GetMethod("WriteLine", new Type[] { typeof(string) }),
            Expression.Constant("before `f`")),

        f.Body,

        Expression.Call(
            null,
            typeof(Console).GetMethod("WriteLine", new Type[] { typeof(string) }),
            Expression.Constant("after `f`"))
        );

    var lambda = Expression.Lambda<Action>(exp);
    lambda.Compile()();
}

트리 순회하기

static void Rini(Expression f)
{
    Console.WriteLine(f);

    if (f is MethodCallExpression)
    {
        var body = f as MethodCallExpression;

        foreach (var arg in body.Arguments)
            Rini(arg);
    }
    if (f is BinaryExpression)
    {
        var body = f as BinaryExpression;

        Rini(body.Left);
        Rini(body.Right);
    }
}
static void Rini(Expression<Action> f)
{
    Rini(f.Body);
}

Expression은 순회와, 수정에 적합한 구조는 아닙ㄴㅣ디ㅏ.

'C#' 카테고리의 다른 글

[C#] Linq.Expression Snippets  (0) 2016.09.22
[C#] Async,Await의 내부  (0) 2016.08.29
[C#] async void / Task  (0) 2016.07.18
[HTTP] 웹소켓 핸드쉐이킹  (2) 2016.06.28
[C#] Mono 환경인지 구분하기  (0) 2016.06.21
[NUnit] Callback 방식의 API 테스트하기  (1) 2016.06.16
Posted by pjc0247
TAG C#, LINQ
쓰다말음


위의 경우처럼 함수의 인자를 줄이기 위해서 람다식을 사용하는 경우가 있다.
c++의 std::bind 같은 메소드가 제공되면 편하겠지만, C#은 그러한 메소드를 제공하지는 않는다. 정상적인 코드라면 그러한 메소드를 사용하는것보다 람다함수의 캡쳐를 쓰는게 훨씬 낫기 때문에.

하지만 런타임에 위 코드처럼 어떤 메소드의 인자를 줄이고 싶은 상황에서는 상당히 불편하다.

따라하기 위해서는 먼저 C# 컴파일러가 람다와 캡처를 어떻게 처리하는지를 알아야 할 필요가 있다. (보고 배끼기 위해서)


위와같은 코드를 디컴파일하면 아래와같은 IL 코드가 생성된다.

주목할 점은 캡처를 클래스 필드를 이용해서 수행한다는 점인데, 코드중 string message 부분이다.
실제 람다 본문인 b__0 에서는 ldfld를 이용해 필드에 접근한다.


이를 Emit 으로 따라한 코드


caller side

callee side





'C# > MSIL' 카테고리의 다른 글

[MSIL] 람다식의 캡쳐 구현하기  (0) 2016.09.01
CLR에서 돌아가는 언어를 뭔가를 만들어 보면서  (1) 2015.11.17
[MSIL] 런타임 property 구현하기  (1) 2015.11.05
[MSIL] beforefieldinit  (1) 2015.11.02
[MSIL] for loop  (1) 2015.11.02
Posted by pjc0247
TAG C#, MSIL