Search Unity

유니티의 스크립팅 가상 머신 팀은 코드 실행 속도를 더 높일 수 있는 방법을 늘 모색한다. 이번 게시물은 IL2CPP AOT 컴파일러가 수행하는 마이크로 최적화의 정의와 활용 방법에 관한 연재물 3부 중 1부이다. 지금부터 소개할 방법이 코드 실행 속도를 두 세 배로 끌어올리지는 않겠지만 약간의 최적화가 게임의 중요한 부분에 도움이 될 수 있으며, 여러분이 코드 실행 과정을 이해하는 기회가 될 수 있다.

최신 컴파일러는 런타임 코드 성능을 개선하기 위한 각종 최적화 수행에 탁월하다. 개발자로서 우리는 코드에 관한 정보를 컴파일러에 명시하여 컴파일러 성능을 향상시킬 수 있다. 오늘은 IL2CPP의 마이크로 최적화 중 한 방법을 비교적 자세히 다루고 기존 코드의 성능 개선에 어떤 도움이 될지 살펴보겠다.

 

Devirtualization

가상 메서드 호출은 무조건 직접 메서드 호출보다 비싸다는 표현 외에 다른 말은 떠오르지 않는다. libil2cpp 런타임 라이브러리의 성능을 개선하여 가상 메서드 호출의 오버헤드를 줄이기 위해 노력했지만(자세한 내용은 다음 게시물에서 다룸), 아직은 런타임 조회가 필요하다. 컴파일러는 런타임에 어떤 메서드가 호출될지 알 수 없다. 과연 그럴까?

역가상화는 가상 메서드 호출을 직접 메서드 호출로 변경하는 일반적인 컴파일러 최적화 전술이다. 컴파일러가 컴파일 시간에 정확히 어떤 실제 메서드가 호출될지 입증할 수 있어야 이 전술을 적용할 수 있다. 안타깝게도 컴파일러가 전체 코드베이스를 항상 볼 수는 없기 때문에 이 점은 쉽게 입증할 수 없다. 그러나 이것이 가능하다면 가상 메서드 호출이 훨씬 빨라질 수 있다.

 

전형적인 예시

젊은 개발자에 속하는 나는 다소 부자연스러운 동물 예제로 가상 메서드에 대해 배웠다. 여러분도 아마 이 코드에 익숙할 것이다.

유니티(버전 5.3.5)에서 우리는 이 클래스를 이용해 작은 농장을 만들 수 있다.

여기에서 Speak 호출은 모두 가상 메서드 호출이다. IL2CPP에 이 메서드 호출 중 무엇이든 역가상화하여 성능을 개선하도록 명령할 수 있는지 알아보자.

 

C++ 코드생성도 나쁘지 않다

내가 좋아하는 IL2CPP 기능 중 하나는 어셈블리 코드 대신 C++ 코드를 생성하는 기능이다. 물론 이 코드는 여러분이 직접 작성한 C++ 코드처럼 보이지는 않지만 어셈블리보다 훨씬 이해하기 쉽다. ForEach 루프의 본문에 대해 생성된 코드를 살펴보자.

간단하게 설명하기 위해 생성된 코드 중 일부를 지웠다. 거슬리는 Invoke 호출이 보이는지? Vtable에서 적절한 가상 메서드를 조회한 후 호출할 것이다. Vtable 조회는 직접 함수 호출보다 느리겠지만 이해할 만하다. 동물은 소나 돼지 또는 다른 파생된 유형일 수 있다.

이제 직접 메서드 호출에 더 가까운 두 번째 Debug.LogFormat 호출에 대해 생성된 코드를 살펴보자.

이 경우에도 가상 메서드 호출은 여전히 가능한다! IL2CPP는 최적화에 아주 보수적이어서 대부분의 경우 정확성 확인을 선택한다. 직접 호출일 가능성을 확인하기 위한 전체 프로그램 분석을 충분히 하지 않기 때문에 더 안전한 (그리고 더 느린) 가상 메서드 호출을 선택한다.

우리 농장에는 다른 종류의 소가 없어서 다른 종류의 소가 파생되지 않을 것임을 알고 있다고 가정해 보자. 이 사실을 컴파일러에 명시하면 더 나은 결과를 얻을 수 있다. 정의할 클래스를 이렇게 변경해 보자.

봉인된 (sealed) 키워드가 컴파일러에게 소에서 아무 것도 파생되지 않음을 알린다(봉인됨 역시 Speak 메서드에 직접 사용할 수 있음). 이제 IL2CPP가 직접 메서드 호출에 대한 확신을 갖게 된다.

컴파일러에 정보를 명시했고 확실히 최적화할 수 있도록 했기 때문에, 여기에서 Speak 호출은 크게 느리지 않을 것이다.

이런 종류의 최적화가 게임 속도를 눈에 띄게 높이지는 않겠지만, 향후 이 코드를 읽을 사람과 컴파일러 모두에게 코드 안의 코드에 관한 여러분의 가정을 명시하는 좋은 방법이 될 수 있다. IL2CPP로 컴파일링할 때는 생성된 C++ 코드를 프로젝트에서 숙독하여 다른 새로운 사실을 발견하길 바란다!

다음에는 왜 가상 메서드 호출이 비싼지, 속도를 높이기 위한 방법은 무엇인지 알아보겠다.

60 replies on “IL2CPP 최적화: Devirtualization”

Hi, does this optimalization work if I call this.Speak() inside of Cow class?
Does this optimalization work in Mono as well?

A related more complicated example: Does boxing get optimized out with generics in both mono and IL2CPP?

T Convert(U other)
{
return (T)(object)(other);
}

Finally, sealed is used for optimization! As far I know, Microsoft guys had some low-priority plans to use it for optimization in their JIT but never got down to it.

Nice article but some questions to you:
Assume that we have a class derived from Cow

public class FlyingCow : Cow{
public override string Speak() {
return “Moooooooo”;
}
}

and class hierarchy is next:
Animal (base class) -> Cow -> FlyingCow

in this case we need use “sealed” keyword ONLY in “top” class (in my example FlyingCow) and this keyword will not work in Cow class (IL2CPP will generate C++ code with template) RIght ?

If I’m not mistaken, there are 2 possible cases, depending on the specifications of your Flying cow: does it make the same sound than the Cow or not?

1) If they make different sounds (“Moo” vs “Moooooooo”), it’s the case you state. Flying cow derives from Cow, so obviously the Cow class can’t be sealed. You can only seal the Flying cow class, so just as you stated this class will be the only one benefiting from the devirtualization.

2) Now, if they make the same sound (“Moo”) but the difference lies somewhere else in the class, what you can do is: seal the Flying cow class just like before so it still benefits from the entire devirtualization; but also seal the method Speak() on the Cow class (and of course don’t override it in the Flying cow class, because it’s the same) and then it should still be devirtualized.

C++ also offers virtual and override features. Why implementing a “virtual function invoker” instead of simply translating C# to C++ classes? As I understand, in pure C++ the difference then would be just a matter of 4 lines of assembly code against 1. Is that also the case explained in the article?
Thanks! Best regards :)

Hi Josh, thanks for post.

About your example, how about using interface to instead of abstract class/method?

Something like:
IAnimal animal = new Cow();
animal.Speak()?

Is that the same problem for generating c++ code?

Hi Josh,

Do you know have any information on what happens to unused code? I have in my project tons of debug and test code that will never be called in production, but when I check the assemblies (inside the android APK), all that code is still there.

I wonder if the IL2CPP or the platform compiler removes unused code. I expect that less code would make the game launch a bit faster and the package smaller as well.

Cheers.

So this optimization only saves you time if you are calling the method from the inherited class, correct? It does not benefit when you call Animal.Speak()?

Any chance we will see IL2CPP on PC standalone builds anytime soon? I know it’s in the Windows Store builds already… what’s the hold up? Our Garbage Collection is causing hitches and I’ve heard IL2CPP will help that a lot. Love the blog post, by the way.

Does this have any effect on classes directly inheriting from MonoBehaviour, like Farm in the example?
I know the usual Unity methods like Start, Awake and Update aren’t overrides of an abstract method, but is there something else thats ‘abstract’ in MonoBehaviour?

Must admit this is a pretty interesting read as I’m very interested in the IL2CPP, although I’m not sure how it entirely works behind the covers. Additional I’ve been wondering for some times why Unity doesn’t simply allow you to make C++ script which can be used like the C#/UnityScritpt/boo scripts native, any wouldn’t this over all be a huge performance gain for skilled C++ programmers?

I found these the fastest versions of “for”, since list.Count is evaluated only once; rather than every loop iteration.

for (int i=list.Count-1; i>=0; –i) …
for (int i=0, iend = list.Count; i<iend; ++i) …

Where the decrementing version could run faster on platforms that support the "subs" instruction (eg ARM devices), which means there could be one "cmp" operation less per loop-iteration.

foreach is often considered the slowest method to iterate over a list, because it allocates an enumerator and it calls various methods each iteration (Next and Current).

However, you will never know, if you don't profile your code.

Why can’t you check if class doesn’t have subclasses and then don’t require “sealed” keyword? Linked libraries?

Great Article, please keep more like this coming.
I know you plan to have other optimizations discussed, but can you point to a resource (like in the documentation) that talks more about what we can do to make our coding practices better for cpp?

One day I would love to know the purpose of;

int32_t L_6 = V_3;
int32_t L_7 = L_6;

Just passing int around? Sounds wasteful.

Nice post, however i am not sure what tangible gains will something like this give. Is the cost of virtual vs. direct method call in IL2CPP really that different?

Yes, the difference is quite notable when in a restrictive environment like iOS, DS or PSP.

A VTable lookup can eat quite a few valuable cycle that would be better used somewhere else. I worked on a PSP game where class hierarchy were destroyed and merged on purpose to reduce the number of lookup in code path that were most used per frame.

Grate Thanks. And If is posible, a video tutorial introduction to IL2CPP in the learning live training section?

Pretty neat optimisation. And kinda obvious too, but appreciated nonetheless ;-) . At this point I’m wondering if you’re beginning to see the value in open sourcing IL2CPP. The community can figure out many more optimisations at many times the speed at which you’re currently doing it. Do the cons really outweigh the pros of open sourcing?

Comments are closed.