Search Unity

This is the fourth blog post in the IL2CPP Internals series. In this post, we will look at how il2cpp.exe generates C++ code for method calls in managed code. Specifically, we will investigate six different types of method calls:

  • Direct calls on instance and static methods
  • Calls via a compile-time delegate
  • Calls via a virtual method
  • Calls via an interface method
  • Calls via a run-time delegate
  • Calls via reflection

In each case, we will focus on what the generated C++ code is doing and, specifically, on how much those instructions will cost.

As with all of the posts in this series, we will be exploring code that is subject to change and, in fact, is likely to change in a newer version of Unity. However, the concepts should remain the same. Please take everything discussed in this series as implementation details. We like to expose and discuss details like this when it is possible though!

Setup

I’ll be using Unity version 5.0.1p4. I’ll run the editor on Windows, and build for the WebGL platform. I’m building with the “Development Player” option enabled, and the “Enable Exceptions” option set to a value of “Full”.

I’ll build with a single script file, modified from the last post so that we can see the different types of method calls. The script starts with an interface and class definition:

Then we have a constant field and a delegate type, both used later in the code:

Finally, these are the methods we are interested in exploring (plus the obligatory Start method, which has no content here):

 

With all that defined, let’s get started. Recall that the generated C++ code will be located in the Temp\StagingArea\Data\il2cppOutput directory in the project (as long as the editor remains open). And don’t forget to generate Ctags on the generated code, to help navigate it.

Calling a method directly

The simplest (and fastest, as we will see) way to call a method, is to call it directly. Here is the generated code for the CallDirectly method:

The last line is the actual method call. Note that it does nothing special, just calls a free function defined in the C++ code. Recall from the earlier post about generated code, that il2cpp.exe generates all methods as C++ free functions. The IL2CPP scripting backend does not use C++ member functions or virtual functions for any generated code. It follows then that calling a static method directory should be similar. Here is the generated code from the  CallStaticMethodDirectly method:

We could say there is less overhead calling a static method, since we don’t need to create and initialize an object instance. However, the method call itself is exactly the same, a call to a C++ free function. The only difference here is that the first argument is always passed with a value of NULL.

Since the difference between calls to static and instance methods is so minimal, we’ll focus on instance methods only for the rest of this post, but the information applies to static methods as well.

Calling a method via a compile-time delegate

What happens with a slightly more exotic method call, like an indirect call via delegate? We’ll first look at a what I’ll call a compile-time delegate, meaning that we know at compile time which method will be called on which object instance. The code for this type of call is in the CallViaDelegate method. It looks like this in the generated code:

 

 

I’ve added a few comments to indicate the different parts of the generated code.

Note that the actual method called here is not part of the generated code. The method VirtFuncInvoker1<int32_t, String_t*>::Invoke is located in the GeneratedVirtualInvokers.h file. This file is generated by il2cpp.exe, but it doesn’t come from any IL code. Instead, il2cpp.exe creates this file based on the usage of virtual functions that return a value (VirtFuncInvokerN) and those that don’t (VirtActionInvokerN), where N is the number of arguments to the method.

The Invoke method here looks like this:

 

 

The call into libil2cpp GetVirtualInvokeData looks up a virtual method in the vtable struct generated based on the managed code, then it makes a call to that method.

Why don’t we used C++11 variadic templates to implement these VirtFuncInvokerN methods? This looks like a situation begging for variadic templates, and indeed it is. However, the C++ code generated by il2cpp.exe has to work with some C++ compilers which don’t yet support all C++ 11 features, including variadic templates. In this case at least, we did not think that forking the generated code for C++11 compilers was worth the additional complexity.

But why is this a virtual method call? Aren’t we calling an instance method in the C# code? Recall that we are calling the instance method via a C# delegate. Look again at the generated code above. The actual method we are going to call is passed in via the MethodInfo* (method metadata) argument: ImportantMethodDelegate_Invoke_m5_MethodInfo. If we search for the method named “ImportantMethodDelegate_Invoke_m5” in the generated code, we see that the call is actually to the managed Invoke method on the ImportantMethodDelegate type. This is a virtual method, so we need to make a virtual call. It is this ImportantMethodDelegate_Invoke_m5 function which will actually make the call to the method named Method in the C# code.

Wow, that was certainly a mouth-full. By making what looks like a simple change to the C# code, we’ve now gone from a single call to a C++ free function to multiple function calls, plus a table lookup. Calling a method via a delegate is significantly more costly than calling the same method directly.

Note that in the process of looking at a delegate method call, we’ve also seen how a call via a virtual method works.

Calling a method via an interface

It’s also possible to call a method in C# via an interface. This call is implemented by il2cpp.exe similar to a virtual method call:

Note the actual method call here is done via the InterfaceFuncInvoker1::Invoke function, which is in the GeneratedInterfaceInvokers.h file. Like the VirtFuncInvoker1 class the InterfaceFuncInvoker1 class does a lookup in a vtable via the il2cpp::vm::Runtime::GetInterfaceInvokeData function in libil2cpp.

Why does an interface method call need to use a different API in libil2cpp from a virtual method call? Note that the call to InterfaceFuncInvoker1::Invoke is passing not only the method to call and its arguments, but also the interface to call that method on (L_1 in this case). The vtable for each type is stored so that interface methods are written at a specific offset. Therefore, il2cpp.exe needs to provide the interface in order to determine which method to call.

The bottom line here is that calling a virtual method and calling a method via an interface have effectively the same overhead in IL2CPP.

Calling a method via a run-time delegate

Another way to use a delegate is to create it at runtime via the Delegate.CreateDelegate method. This approach is similar to a compile-time delegate, except that it be modified at runtime in a few more ways. We pay for that flexibility with an additional function call. Here is the generated code:

 

 

This delegate requires a good bit of code for creation and initialization. But the method call itself has even more overhead, too. First we need to create an array to hold the method arguments, then call the DynamicInvoke method on the Delegate instance. If we follow that method in the generated code, we can see that it calls the VirtFuncInvoker1::Invoke function, just as the compile-time delegate does. So this delegate requires one more function call then the compile-time delegate does, plus two lookups in a vtable, instead of just one.

Calling a method via reflection

The most costly way to call a method is, not surprisingly, via reflection. Let’s look at the generated code for the CallViaReflection method:

 

 

As in the case of the runtime delegate, we need to spend some time creating an array for the arguments to the method. Then we make a virtual method call to MethodBase::Invoke (the MethodBase_Invoke_m24 function). This function in turn invokes another virtual function, before we finally get to the actual method call!

Conclusion

While this is no substitute for actual profiling and measurement, we can get some insight about the overhead of any given method invocation by looking at how the generated C++ code is used for different types of method calls. Specifically, it is clear that we want to avoid calls via run-time delegates and reflection, if at all possible. As always, the best advice about making performance improvements is to measure early and often with profiling tools.

We’re always looking for ways to optimize the code generated by il2cpp.exe, so it is likely that these method calls will look different in a later version of Unity.

Next time we’ll delve deeper in to method implementations and see how we share the implementation of generic methods to minimize generated code and executable size.

14 Comments

Subscribe to comments

Comments are closed.

  1. Will Unity use LLILC?

  2. Dear Unity Team. When we can see IL2CPP on PC platform?

  3. Martin Kroslak

    June 9, 2015 at 10:21 am

    Hello, I’m currently dealing with similar issue (generating portable C++ from IL assemblies) and I was wondering whether it is possible to use your converter for projects not using Unity engine. I imagine it is possible from technical standpoint, but I’m also asking about legal issues.

  4. Still no love for this bug? Are 3 weeks too early to expect a reaction? http://fogbugz.unity3d.com/default.asp?696840_1ukc8ppg8btktrh6

    1. Josh Peterson

      June 5, 2015 at 1:32 pm

      We’ve not investigated that bug yet, sorry. It looks pretty straight-forward though. We will have a look at it. Thanks.

  5. hello, i have the big problem about this “Next time we’ll delve deeper in to method implementations and see how we share the implementation of generic methods to minimize generated code and executable size.”

    My app is add 50M after use IL2CPP…

    is’t because that i use too many List ??

  6. I have the big problem about this “Next time we’ll delve deeper in to method implementations and see how we share the implementation of generic methods to minimize generated code and executable size.”

    My app is add 50M after use IL2CPP…

    is’t because that i use too many List ??

    1. Josh Peterson

      June 4, 2015 at 2:29 pm

      Note that application sizes with IL2CPP will be larger, because the default “Architecture” option in the iOS player settings is set the “Universal”, meaning we will build binaries for both ARMv7 (32-bit) and ARM64 (64-bit).

      IL2CPP executable sizes in the latest Unity releases are smaller than Mono for the ARMv7 slice and larger than Mono for the ARM64 slice (on most projects we have seen). Overall, IL2CPP sizes are still larger than Mono. This is a key area of development for us now.

      Use of too many List types will likely not increase your binary size, due to the generic sharing (which we’ll explore in the next post).

      1. thanks your reply, it’s help.

  7. Kill me now !!!!!!

    Dear god, why are you guys showing us all that, last time I recall Unity was a user friendly engine, what happened !!!!

    1. I guess you have missed the point… this is a blog post for people who are interested whats happening behind the scenes.

    2. Josh Peterson

      June 4, 2015 at 2:25 pm

      The good news is that you can safely ignore all of this. :)

      With IL2CPP, we’re not changing anything about how to use Unity. But we wanted to take the opportunity to expose some of the internal details. Note that things are just as complex with the Mono scripting backend (or more). Remember, it is emitting platform-specific assembly code in these cases!

      1. “The good news is that you can safely ignore all of this. :)”

        That is not true. You can not ignore the details for the same reason that you can not e.g. ignore the internals of the “mono garbage collector in Unity causing game stuttering”.

        Sure, you can try to be blind about “soft goal characteristics” but then your games will be sluggy, big, inperformant and probably buggy.

        So please stop thinking of these “internal revelations” as some nice bonus and think more about some “required documentation”. Also consider making more appropiate documentation formats for this information presented here.

        John: What IS true, is that not all of us need to know all of the details. If you are an artist working with some coders, point them to this article and keep using the awesome editor platform that Unity provides ;).

    3. John, it’s really obvious that they’re showing the internal details for those interested. For the rest of us, it’s exposing the detailed work they have to do to keep it a “user friendly engine”.