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.