게임엔진/유니티

IL2CPP가 Virtual Call과 Boxing을 처리하는 방법

tsyang 2023. 11. 19. 12:29

이 글의 정보들은 2016년에 작성된 글을 기반으로 함. 따라서 2023년 현재 바뀐 부분이 있을 수 있음.

 

Devirtualization 

 

당연한 얘기지만, Virtual Call은 Direct Call보다 더 느리다. 

 

따라서 일부 컴파일러는 Virtual Call을 Direct Call로 바꾸는 Devirtualization 기법을 사용하기도 한다. 단, 해당 코드가 컴파일 타임에 어떤 메서드를 실행시킬지를 판단할 수 있어야 한다.

 

이는 IL2CPP도 마찬가지이다. 다만 IL2CPP는 최적화에 보수적이기 때문에...

var dog = new Dog();    //Dog는 Animal 클래스를 상속함.
dog.Speak();

 

위와 같은 상황에서도 Virtual Call을 호출하는 C++코드를 만들어낸다. (2016년에 쓰여진 글 기준)

 

그러나 sealed 키워드를 붙여주면 확실하게 direct call을 호출하는 코드를 생성하기 때문에 가급적 sealed 키워드를 붙여주는 것이 좋다.

 

 


 

Virtual Call

 

Virtual Method Call의 대상 Method는 런타임에야 결정된다. 따라서 컴파일러는 컴파일타임에 어떤 메서드를 실행시킬지 알 수 없기 때문에, 우리가 vtable이라 부르는 메서드의 배열을 만들어낸다.

 

런타임에 Virtual Method Call이 호출되면, vtable을 참고하여 어떤 메서드를 실행할지를 결정하는 것이다.

 

그런데 이 vtable에 내가 실행하려는 메서드가 없는 경우는 어떻게 될까?

 

class Base
{
    public virtual string Speak()
    {
        return "Base";
    }
}

class Derived<T> : Base
{
    public override string Speak()
    {
        return "Derived";
    }
}

 

 

위와 같은 클래스가 있다고 하자. 

 

public class VirtualInvokeExample
{
    void Start()
    {
      //런타입에 새로운 타입을 만든다. 이 코드를 제외한 곳에서는 Derived<int>가 없다고 가정한다.
        var derivedType = typeof(Derived<>).MakeGenericType(typeof(int));
        var runtimeBaseClass = (Base) FormatterServices.GetUninitializedObject(derivedType);  

        Debug.Log(MakeRuntimeBaseClass().SayHello());
    }
}

 

그리고 위와 같이 런타임에 새로 Derived<int> 타입의 인스턴스를 생성하고, Speak()메서드를 호출한다면?

 

JIT에서는 문제가 없다. 왜냐? Derived<int> 타입을 처음 만났을 때 vtable을 만들어 버리기 때문이다.

 

그러나 이건 AOT에서는 문제가 된다. 왜냐? Derived<int>타입의 vtable을 컴파일타임에 생성하지 않았기 때문이다. 따라서 이런 경우에는  AOT code not generated 익셉션이 떠야한다.

 

그렇다면 IL2CPP에서는 내부적으로 virtual call을 하는 메서드를 어떻게 생성해냈을까?

 

static inline void GetVirtualInvokeData(Il2CppMethodSlot slot, void* obj, VirtualInvokeData* invokeData) {
   *invokeData = ((Il2CppObject*)obj)->klass->vtable[slot];
   if (!invokeData->methodPtr)
       RaiseExecutionEngineException(invokeData->method);
}

 

간단하다. 해당 타입의 vtable을 뒤져서 메서드를 찾고, 메서드가 없으면 익셉션을 발생시킨다. 

 

이대로도 문제 없어 보이지만, 위 메서드는 모든 virtual call에 호출되는 녀석이기 때문에 엄청 많이 호출된다. 메서드를 더 최적화할 수 있을까? 물론 가능하다. 어떻게? IF문을 제거한다면 비교연산을 줄이고, branch prediction의 이득을 볼 수도 있다. 

 

IL2CPP에서는 모든 메서드 시드니처를 vtable에 생성해버린다. 따라서 vtable에서 메서드를 찾지 못하는 경우가 없음이 보장된다. 대신 이런 방식으로 추가된 메서드들은 AOT code not generated exception을 발생시킨다. 이러면 if문을 제거해도 예전과 동일하게 동작할 수 있다.

 

이렇게 if문을 제거했을 뿐인데 전체적인 속도가 3~4%향상했다고 한다. branch prediction을 잘 활용할 수 있기 때문.

 

 


 

Boxing

 

박싱이 일어나는 코드라면, 가능한 경우 IL2CPP가 Value Type 한정으로 메서드를 별도로 생성하기도 한다고 한다. 

 

 


 

 


참고 : 

https://blog.unity.com/technology/il2cpp-optimizations-devirtualization

https://blog.unity.com/technology/il2cpp-optimizations-faster-virtual-method-calls

https://blog.unity.com/technology/il2cpp-optimizations-avoid-boxing