IL2CPP Internals: Generic sharing implementation
This is the fifth post in the IL2CPP Internals series. In the last post, we looked at how methods are called in the C++ code
generated for the IL2CPP scripting backend. In this post, we will explore how they are implemented. Specifically,
we will try to better understand one of the most important features of code generated with IL2CPP - generic sharing.
Generic sharing allows many generic methods to share one common implementation. This leads to significant decreases in executable
size for the IL2CPP scripting backend.
Note that generic sharing is not a new idea, both Mono and .Net runtimes use generic sharing as well. Initially, IL2CPP did not
perform generic sharing. Recent improvements have made it even more robust and beneficial. Since il2cpp.exe generates C++ code,
we can see where the method implementations are shared.
We will explore how generic method implementations are shared (or not) for reference types and value types. We will also investigate
how generic parameter constraints affect generic sharing.
Keep in mind that everything discussed in this series are implementation details. The topics and code discussed here are likely
to change in the future. We like to expose and discuss details like this when it is possible though!
What is generic sharing?
Imagine you are writing the implementation for the List
class in C#. Would that implementation depend on the type T is?
Could you use the same implementation of the Add method for List<string>
and List<object>? How about List<datetime>?
In fact, the power of generics is just that these C# implementations can be shared, and the generic class List
will work for any T. But what happens when List is translated from C# to something executable, like assembly code
(as Mono does) or C++ code (as IL2CPP does)? Can we still share the implementation of the Add method?
Yes, we can share it most of the time. As we’ll discover in this post, the ability to share the implementation of a
generic method depends almost entirely on the size of that type T. If T is any reference type (like string or object),
then it will always be the size of a pointer. If T is a value type (like int or DateTime), its size may vary, and things
get a bit more complex. The more method implementations which can be shared, the smaller the resulting executable code is.
Mark Probst, the developer who implemented generic sharing Mono, has an excellent series of
posts on how Mono
performs generic sharing. We won’t go into that much depth about generic sharing here. Instead, we will see how and
when IL2CPP performs generic sharing. Hopefully this information will help you better analyze and understand the executable
size of your project.