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

원본 : 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

[C#] Async,Await의 내부

C# 2016.08.29 15:32

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


behind_async_await

C#은 stackless한 코루틴을 지원합니다. 어케 가능한지 아라보자

static async void Foo()
{
    Console.WriteLine("A");

    await Task.Delay(1000);

    Console.WriteLine("B");

    await Task.Delay(1000);

    Console.WriteLine("C");
}

아래의 코드는 Foo 함수를 디컴파일 한 결과물을 기반으로 다시 작성되었습니다. 대충 필요한것만 남기고 생략함

class MyAsync : System.Runtime.CompilerServices.IAsyncStateMachine
{
    internal AsyncVoidMethodBuilder builder { get; set; }
    internal int ptr { get; set; }

    public void MoveNext()
    {
        var _this = this;

        switch(ptr)
        {
            case 0:
                {
                    Console.WriteLine("A : " + ptr.ToString());
                    var task = Task.Delay(1000).GetAwaiter();
                    builder.AwaitUnsafeOnCompleted(ref task, ref _this);

                    ptr++;
                    break;
                }

            case 1:
                {
                    Console.WriteLine("B : " + ptr.ToString());
                    var task = Task.Delay(1000).GetAwaiter();
                    builder.AwaitUnsafeOnCompleted(ref task, ref _this);

                    ptr++;
                    break;
                }

            case 2:
                {
                    Console.WriteLine("C : " + ptr.ToString());
                    break;
                }
        }
    }

    public void SetStateMachine(IAsyncStateMachine stateMachine)
    {
    }
}

var async = new MyAsync();
var builder = AsyncVoidMethodBuilder.Create();
async.ptr = 0;
async.builder = builder;
builder.Start(ref async);
  • async가 붙은 메소드는 IAsyncStateMachine를 구현하는 클래스로 변환됩니다.
  • IAsyncStateMachine는 MoveNext 메소드를 정의합니다.
  • MoveNext는 계속 실행됩니다. MoveNext 내부의 상태(실행 포인터) 관리는 클래스 내부의 구현체에서 직접 해야합니다.
  • AwaitUnsafeOnCompleted는 어떠한 Task가 완료되면 다시 MoveNext를 호출하도록 예약하는 역할을 합니다.
  • 이 작업이 반복되면서 await/async가 동작하게 됩니다.

결론

  • stackless 코루틴은 stackful 코루틴처럼 사기군같은 컨텍스트 스위칭 없이 자체 상태머신을 이용한 멀쩡해보이는 방법으로 구현한다.
  • 컴파일러가 자동으로 코드를 길게 풀어준다.
  • 사기군같은 stackful 코루틴은 https://github.com/pjc0247/jwgtrich


'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 AsyncAwait, C#

[C#] async void / Task

C# 2016.07.18 14:50

공통점
* 둘다 메소드 본문이 다 실행되기도 전에 리턴한다. 정확히는 첫번째 await 를 만나기 전까지는 계속 실행되고, 첫번째 await를 만나면 리턴한다.

void 의 경우
* 익셉션이 발생하면 UnhandledException으로 간주되고 프로그램이 종료된다. (.NET 런타임이 이 메소드가 익셉션을 내는지 안내는지를 감시한다)

Task 의 경우
* .NET 런타임이 감시하지 않는다. 유저가 예외처리를 해야 함

* 발생한 익셉션은 리턴값 TaskException에 채워진다 

* 또는 await 사용시, 해당 시점에서 익셉션이 발생한다.

* (유저가 예외처리를 해야하지만 암것도 안했을 경우) 익셉션이 발생해도 아무일도 없는 것 처럼 보인다. 

* 암것도 안했을 경우 TaskScheduler.UnobservedTaskException 콜백이 실행된다. (익셉션 발생 즉시 실행되지 않고 GC가 Task를 정리하려고 했는데, 유저가 익셉션 처리를 안한 경우에 트리거된다.)




언제 써야 할까

void : 최상위 작업인 경우, 이 메소드는 대기될 필요도 없어야 하고, 실제로 async void는 대기시킬 방법도 없다. async void는 단순히 그냥 함수인데 await 키워드를 쓰고싶은 함수라고 간주하면 된다. 주로 이벤트 핸들러에 사용된다.

Task : 이 메소드는 대기될 수 있다. 이 메소드 호출자가 이 메소드 실행을 기다려야(await) 할 필요가 있는 경우, 메소드 이후에 연속된 작업(ContinueWith)이 있어야 하는 경우. Task는 단순히 Task<Void> 라고 생각하면 된다.


'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 async, C#
먼저 클라이언트에서 웹소켓 오픈 시, 서버로 아래와 같은 요청이 온다.
(일반적인 HTTP 요청의 형태에 몇몇 추가적인 헤더를 담아서 보내는 방식이다.)


여기서 살펴보야아 할 값은 Sec-WebSocket-Key인데, 이 값은 서버가 웹소켓 프로토콜을 제대로 이해하고 있는지를 구별하는 역할을 한다.

이제 서버에서는 프로토콜 업그레이드에 대한 응답을 보내주어야 한다.
상태 코드는 101번이고, 반드시 Sec-WebSocket-Accept 헤더에 알맞은 값을 채워서 보내야한다.

클라이언트로부터 받은 Sec-WebSocket-Key 값에 고정 GUID "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" 를 이어 붙인 후 SHA-1 해싱 -> base64로 인코딩한다.
(위의 GUID는 웹소켓 스펙에 정해진 고정 문자열이다.)

아래는 올바른 리스폰스의 예시 


이 요청을 보낸 후부터는 HTTP/1.1 프로토콜이 아닌 웹소켓 프로토콜로 데이터를 주고받는다.



이걸 C# 코드로 구현하면 아래와 같다.





Posted by pjc0247
TAG C#, HTTP, WebSocket

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


가끔 모노에서만 오작동하는 코드를 분기처리하기 위해


Posted by pjc0247
TAG C#, Mono

Setup 등의 메소드에서는 async 키워드를 사용할 수 없다.
t.Task.Wait() 메소드를 호출해서 강제로 대기해야 한다.


Posted by pjc0247
TAG C#, NUnit