Search Unity

이번 IL2CPP 마이크로 최적화 미니시리즈의 마지막 에피소드에서는 “박싱”이라는 고비용 유발 요인을 살펴보고 IL2CPP에서 어떻게 하면 불가피한 경우를 제외하면서 박싱을 피할 수 있는지 살펴본다.

힙(Heap) 할당이 느려진다

많은 프로그래밍 언어가 그러하듯 C#에서는 스택(stack)(작고 “빠르며” 범위가 정해진 메모리 블록)이나 힙(heap)(크고 “느린” 광역 메모리 블록)에 객체용 메모리를 할당할 수 있다. 일반적으로 힙에 객체용 공간을 할당하는 것이 스택에 공간을 할당하는 것보다 훨씬 더 많은 비용이 소요된다. 또한 힙에 객체용 공간을 할당한 경우 할당된 메모리를 가비지 컬렉터(garbage collector)에서 추적하는 작업이 수반되기 때문에 추가 비용도 발생한다. 따라서 가급적이면 힙 할당을 피하는 것이 바람직하다.

C#을 이용하면 형식을 (스택에 할당할 수 있는) 값 형식과 (반드시 힙에 할당해야 하는) 참조 형식으로 나눌 수 있기 때문에 힙 할당을 피할 수 있다. int와 float 같은 형식은 값 형식에 해당되며 string가 object는 참조 형식에 속한다. 사용자 정의형 값 형식에는 struct라는 키워드가 사용되고 사용자 정의형 참조 형식에는 class라는 키워드가 사용된다. 참고로, 값 형식에는 null 값이 절대로 존재할 수 없다. C#에서 null 값은 참조 형식에만 할당할 수 있다. 진행하는 동안 이런 차이점을 계속 유념해야 한다.

만족할만한 성능을 얻으려면 불가피한 경우를 제외하고 박싱을 피하는 것이 바람직하다. 그러나 스택에 할당된 값 형식을 힙에 할당된 참조 형식으로 변환해야 하는 경우도 종종 있다. 이 과정을 박싱(boxing)이라 한다. 박싱 순서는 다음과 같다.

  1. 힙에 공간을 할당한다.
  2. 가비지 컬렉터에게 새 객체에 대해 알린다.
  3. 값 형식 객체의 데이터를 새 참조 형식 객체에 복사한다.

방식은 가급적 기피해야 한다는 사실 잊지 말자!

성가신 컴파일러 (compiler)

불필요한 힙 할당과 방식을 피한 채 즐거운 마음으로 코드를 작성하고 있다고 가정해보자. 당연히 트리도 몇 개 생길 테고 트리의 크기도 지정된 기간에 따라 확장되기 마련이다.

코드의 다른 곳에서는 다음과 같이 편리한 방법으로 (Tree 객체를 포함한) 여러 가지 객체의 크기의 누계를 낼 수 있다.

이 정도면 충분히 안전해 보이지만 C# 컴파일러에 의해 생성되는 IL(Intermediate Language) 코드도 조금만 살펴보자.

C# 컴파일러가 박싱을 통해 if(things[i] != null) 검사 구문을 실행했다! 형식 T가 이미 참조 형식인 경우 opcode 구문에 그리 큰 비용이 들지 않는다. 기존의 포인터를 배열 요소에 반환하기 때문이다. 그러나 T 형식이 (Tree 형식처럼) 값 형식인 경우 opcode 구문에 대단히 많은 비용이 소요된다. 물론, 값 형식에는 절대 null 값이 존재할 수 없다. 그렇다면 검사 구문을 먼저 실행해야 하는 이유는 뭘까? 그리고 100개 혹은 심지어 1000개나 되는 Tree 객체의 크기를 계산해야 하는 경우라면 어떻게 될까? 이럴 때는 불필요해 보이던 박싱이 갑자기 아주 요긴한 수단으로 돌변한다.

실행할 필요가 없는 코드가 가장 빠른 코드

C# 컴파일러는 T 형식이 필요로 하는 일반적 실행 방법을 지원해야 한다. 그러나 IL2CPP같은 컴파일러는 좀 더 “공격적”이어서 실행해야 할 코드는 생성하고 실행할 필요가 없는 코드는 생성하지 않는다!

IL2CPP는 T 형식이 Tree 형식인 경우에 특히 The TotalSize<T> 방법을 실행한다. 위의 IL 코드는 생성된 C++ 코드에서 아래와 같은 양상을 보인다.

값 형식 객체에 절대 null 값이 존재할 수 없다는 사실을 앞서 설명했는데 그런 이유로 IL2CPP는 opcode 구문이 값 형식에 불필요하다고 판단했다. 몇 개의 명령어가 포함된 루프에서 이와 같이 불필요한 할당과 데이터 복사 작업을 배제하면 성능에 상당히 긍정적인 영향을 미칠 수 있다.

요약

이번 시리즈에서 논의했던 다른 마이크로 최적화 방법과 마찬가지로 이번 최적화 방법도 .NET 코드 생성기에서 흔히 사용할 수 있다. 유니티가 현재 사용 중인 모든 Scripting Backend는 이와 같은 최적화를 실시하므로 사용자는 코드 작업에만 전념할 수 있다

모쪼록 마이크로 최적화에 관한 이번 미니시리즈가 도움이 됐기를 바란다. 유니티는 앞으로도 계속 현재 사용 중인 코드 생성기와 런타임을 개선하는 한편, 암암리에 진행되는 마이크로 최적화에 관한 더욱 구체적인 정보를 제공하는 데 힘쓸 것이다.

17 코멘트

코멘트 구독

코멘트를 달 수 없습니다.

  1. In the page Learning, there is nothing about IL2CPP
    https://unity3d.com/learn/tutorials/topics/scripting
    A video that fit in a new ADVANCED GAMEPLAY SCRIPTING section.
    Could cover complex calculating for third person camera position, mesh impact deformation, alternative physics for videogames, aerodynamics drag…

    1. Josh Peterson

      9월 9, 2016 5:03 오후

      Yes, nothing is there yet. I’ll keep your suggestions in mind, and try to determine the best way to present this material. Thanks!

  2. theothermonarch

    8월 16, 2016 1:46 오전

    Not going to work for nullable types I assume.

  3. Robert Cummings

    8월 12, 2016 12:14 오전

    Mad science. Love it.

  4. “IL2CPP will create an implementation of The TotalSize method specifically for the case where T is a Tree.”

    The method is generic, why does it create the implementation for this specific case? not really clear from the post.

    Also, if IL2CPP can make these assumptions, why doesn’t the Mono compiler work in the same way ?

    1. I think what they mean is “IL2CPP will create an implementation of The TotalSize method specifically for the case where T is a a value type ( as is the case for Tree).”

      So they mean, IL2CPP will recognise that Tree is a value type, and react accordingly by omitting the boxing line.

    2. I’m sorry that this isn’t clear, let me try to explain it a bit more. The method is generic in C# code, but when executable code is created by some engine (either JIT, like Mono on desktop, or AOT, like IL2CPP), that engine is responsible to create an implementation of TotalSize for any possible T.

      A JIT engine does this as it encounters each usage of a type for T. An AOT engine inspects all of the code first to find all of the places T is actually used. In both cases, the result is the same – a specific implementation for each type T. Often the engine can share the implementation of the generic method to reduce code size.
      See this post for more details about how IL2CPP does this.

      But at execution time, there is code specifically created for TotalSize so IL2CPP can make this optimization. The Mono C# compiler can’t make this some optimization, since it is not generating executable code. But the Mono JIT and the Mono AOT engine both do make this optimization.

      1. …Just to add, the way generics are implemented in .Net CLR (and by IL2CPP) (i.e. to generate an implementation for any possible T) is in itself an optimisation of sorts. The other known way of doing this is called Type Erasure and is used in the Java Universe. In this case, only one implementation of the generic method is generated. However the type is essentially replaced with the base classObject and applies casts where necessary. This implementation however has many disadvantages compared to the .Net/IL2CPP approach. The only real advantage to type erasure is that the resulting binary could be significantly smaller.

  5. Now that live training are back, is it posible a live training about IL2CPP for beginners?

    1. What kind of topics do you have in mind? Ideally, everything related to IL2CPP should “just work”, as it is pretty low-level and behind-the-scenes, so I’m not sure what we could cover. But I’m open to ideas.

      1. Well In my case my knowledge of IL2CPP is null. So I do not know If is C++ that I can write as a script and attach it in inspector or is a Unity optimisation as for example a new compiler, or C# conversion that is included when we press build (or else) . So a didactic introduction video made by a teacher of what it is in first place and how to use it can help beginners (visual artist with visual skills that also do coding optimisation) to turn the light on it. The ABC. At that point the Unity user can make a decision of investigating more about IL2CPP. There are documentation and videos about IL2CPP (BCDE..) but looks more advance. A live training can be an A.B.C.D.E. where an introduce what IL2CPP is , what is for and how to use it. This blog article can be an example of how to use it. I can understand it. But not how to implement it. Thanks for the consideration.

      2. After explaining the abc you could cover a 3d or 2d vector calculation. In this way is generic and if we want to optimise a random vector, wind, make lift force or light calculations, shadows are cover. Or also to speed up game Start operations when generating procedurals ?

        1. Josh Peterson

          9월 9, 2016 4:40 오후

          I really like these suggestions, Alan. It feels like what we need is some kind of high level overview of how scripting works in Unity. What are the alternatives for users? What are the trade-offs for each alternative? These answers might really help organize the process of thinking about scripting in Unity. You’ve spurred some ideas in my mind about this now. Thanks!

  6. Unfortunately C# does not include a logic that deems constraints as part of the signature of an operation for overload checking, and thus, if you write two TotalSize operations, where one has a contraint “where T: struct, HasSize” and the other “where T: class, HasSize”, the compiler will claim that it is the same operation defined twice.

    Although some may argue whether the “if (things[i] != null)” check should be included in TotalSize or two different names should be assigned the methods (one for class types and another for strcuts), that optimization is indeed imvho a great feature in ILL2CPP.

    So, nice addition. Thanks a lot!

    1. The example code I’ve used here is a bit contrived to show off this optimization, for sure. But we have seen significant performance improvement in real-world projects.

  7. Can IL2CPP prevent boxing when enums are used as keys for dictionaries? I can of course implement the IEqualityComparer, but I’m just curious whether the compiler could do something in this case.

    1. That is a bit more difficult to do. Enum types override ToString, so IL2CPP can’t make many assumptions about when to avoid boxing them, as that could change program behavior. It’s probably best to use IEqualityComparer explicitly.