Search Unity

This is the seventh post in the IL2CPP Internals series. In this post, we will explore a bit about how the IL2CPP runtime integrates with a garbage collector. Specifically, we’ll see how the GC roots in managed code are communicated to the native garbage collector.

As with all of the posts in this series, this post deals with implementation details that can and likely will change in the future. In this post we will specifically look at some internal APIs used by the runtime code to communicate with the garbage collector. These APIs are not publicly supported, and you should not attempt to call them from any code in a real project. But, this is a post about internals, so let’s dig in.

Garbage collection

I won’t discuss general garbage collection techniques in this post, as it is a wide and varied subject, with plenty of existing research and published information. To follow along, just think of a GC as an algorithm that develops a directed graph of object references. If an object Child is used by an object Parent (via a pointer in native code), then the graph looks like this:

image03

As the GC scans through the memory for a process, it looks for objects which don’t have parent. If it finds one, then it can reuse the memory for that object on something else.

Of course, most object will have a parent of some sort, so the GC really needs to know which objects are the important parents. I like to think of these as the objects that are actually in use by your program. In GC terminology, these are called the “roots”. Here is an example of a parent without a root.

image02

In this case, Parent 2 does not have a root, so the GC can reuse the memory from Parent 2 and Child 2. Parent 1 and Child 1, however, do have a root, so the GC cannot reuse their memory. The program is still using them for something.

For .NET, there are three kinds of roots:

  • Local variables on the stack of any thread executing managed code
  • Static variables
  • GCHandle objects

We’ll see how IL2CPP communicates with the garbage collector about all three of these kinds of roots.

The setup

For this post, I’m using Unity 5.1.0p1 on OSX, and I’m building for the iOS platform. This will allow us to use Xcode to have a look at how IL2CPP interacts with the garbage collector. As with the other posts in this series, I’ll use an example project with a single script:

 

 

I have enabled the “Development Build” in the Build Settings dialog, and I set the “Run in Xcode as” option to a value of “Debug”. In the generated Xcode project, first search for the string “Start_m”. You should see the generated code for the Start method in the the HelloWorld class named HelloWorld_Start_m3.

Adding thread local variables as roots

Add a breakpoint in the HelloWorld_Start_m3 function on the line where Thread_Start_m9 is called. This method will create a new managed thread, so we expect that thread to be added to the GC as a root. We can see where this happens by exploring the libil2cpp header files that ship with Unity. In the Unity installation open the Contents/Frameworks/il2cpp/libil2cpp/gc/gc-internal.h file. This file has a number of methods prefixed with il2cpp_gc_ it serves as part of the API between the libil2cpp runtime and the garbage collector. Note that this is not a public API, so please don’t call these methods from any real project code. They are subject to change or removal without notice.

Let’s create a breakpoint in Xcode on the il2cpp_gc_register_thread function, using Debug > Breakpoints > Create Symbolic Breakpoint.

image04

If you then run the project in Xcode, you’ll notice that the breakpoint is hit almost immediately. We can’t see the source code here, as it is built in the libil2cpp runtime static library, but we can see from the call stack that this thread is created in the InitializeScriptingBackend method, which executes when the player starts.

image01

We will actually see this breakpoint hit a number of times, as the player creates each managed thread used internally. For now, you can disable this breakpoint in Xcode and allow the project to continue. We should hit the breakpoint we set earlier in the HelloWorld_Start_m3 method.

Now we are just about to start the managed thread created by our script code, so enable the breakpoint on il2cpp_gc_register_thread again. When we hit that breakpoint, the first thread is waiting to join our created thread, but the call stack for the created thread shows that we are just starting it:

image05

When a thread is registered with the garbage collector, the GC treats all objects on the local stack for that thread as roots. Let’s look at the generated code for the method we run on that thread (HelloWorld_AnotherThread_m4) :

 

We can see one local variable, L_0, which the GC must treat as a root. During the (short) lifetime of this thread, this instance of the AnyClass object and any other objects it references cannot be reused by the garbage collector. Variables defined on the stack are the most common kind of GC roots, as most objects in a program start off from a local variable in a method executing on a managed thread.

When a thread exits, the il2cpp_gc_unregister_thread function is called to tell the GC to stop treating the objects on the thread stack as roots. The GC can then work on reusing the memory for the AnyClass object represented in native code by L_0.

Static variables

Some variables don’t live on thread call stacks though. These are static variables, and they also need to be handled as roots by the garbage collector.

When IL2CPP lays out the native representation of a class, it groups all of the static fields together in a different C++ structure from the instance fields in the class. In Xcode, we can jump to the definition of the HelloWorld_t2 class:

 

Note that IL2CPP does not use the C++ static keyword, as it needs to be in control of the layout and allocation of the static fields to properly communicate with the GC. When a type is first used at runtime, the libil2cpp code will initialize the type. Part of this initialization involves allocating memory for the HelloWorld_t2_StaticFields structure. This memory is allocated with a special call into the GC: il2cpp_gc_alloc_fixed (also in the gc-internal.h file).

This call informs the garbage collector to treat the allocated memory as a root, and the GC dutifully does this for the lifetime of the process. It is possible to set a breakpoint on the il2cpp_gc_alloc_fixed function in Xcode, but it is called rather often (even for this simple project), so the breakpoint is not too useful.

GCHandle objects

Suppose that you don’t want to use a static variable, but you still want a bit more control over when the garbage collector is allowed to reuse the memory for an object. This is usually helpful when you need to pass a pointer to a managed object from managed to native code. If the native code will take ownership of that object, we need to tell the garbage collector that the native code is now a root in its object graph. This works by using a special managed object called a GCHandle.

The creation of a GCHandle informs the runtime code that a given managed object should be treated as a root in the GC so that it and any objects it references will not be reused. In IL2CPP, we can see the low-level API to accomplish this in the Contents/Frameworks/il2cpp/libil2cpp/gc/GCHandle.h file. Again, this is not a public API, but it is fun to investigate. Let’s put a breakpoint on the GCHandle::New function. If we let the project continue then, we should see this call stack:

image00

Notice that the generated code for our Start method is calling GCHandle_Alloc_m11, which eventually creates a GCHandle and informs the garbage collector that we have a new root object.

Conclusion

We’ve looked at some internal API methods to see how the IL2CPP runtime interacts with the garbage collector, letting it know which objects are the roots it should preserve. Note that we have not talked at all about which garbage collector IL2CPP uses. It is currently using the Boehm-Demers-Weiser GC, but we have worked hard to isolate the garbage collector behind a clean interface. We currently have plans to research integration of the open-source CoreCLR garbage collector. We don’t have a firm ship date yet for this integration, but watch our public roadmap for updates.

As usual, we’ve just scratched the surface of the GC integration in IL2CPP. I encourage you to explore more about how IL2CPP and the GC interact. Please share your insights as well.

Next time, we will wrap up the IL2CPP internals series by looking at how we test the IL2CPP code.

10 Comments

Subscribe to comments

Comments are closed.

  1. Oddur Magnusson

    July 23, 2015 at 2:53 pm

    Any thoughts or ideas about migrating to a incremental garbage collection ? Maybe the CLR does not allow such things, but being able to amortize your GC over multiple frames would be an awesome feature.

    Say you have the classic 16ms budget and you are running at 12ms, you could use the remaining 4ms to do GC and avoid the “stop the world” frame spikes. I know this has been a huge advantage for engines that use LUA as their scripting language.

  2. Awesome! This helps me with my hobby project of transpiling C# to C++.
    Have my own GC so I can run C# code on super low memory devices but was confused about how .NET considered variables within a method as roots. This answered a question I was having about that.

  3. This was a great post!

  4. So, the GC of il2cpp is not the same as mono?
    How is the performance of this GC compared to mono one? Better or same?

    1. This version of Mono that ships with Unity also uses BDWGC, although the version of BDWGC used for IL2CPP is a bit newer. So it is the same codebase for the GC as Mono. The performance is similar between the two, based on our tests.

  5. Love this series. Keep up the great work!

    1. Josh Peterson

      July 10, 2015 at 7:37 pm

      Thanks Dave. We only have one more post scheduled, but I’m open to suggestions for additional topics!

  6. Great to know you’re researching into some core CLR components. For one thing, I personally think moving closer to .NetCore and farther from Mono dependence will augur well for Unity. The .Net framework is generally the more stable, production-ready, bigger sibling :-D. Most devs still think of Mono as an experimental project and I dare say it is one of the reasons why some devs don’t take Unity too seriously. Although I suspect that a move like this will only motivate Microsoft to start making billion-dollar offers to acquire Unity, which will ultimately be a bad thing.

  7. Hi!
    I’d like to ask, does the GC have any knowledge of the internal object layouts, so it knows where the pointers are?
    How do you generate this information now that physical layout of fields is basically up to the C++ compiler?

    1. Yes, the GC does know about which fields have pointers. The type metadata for each type has a GC descriptor which is a bit field indicating whether or not there is a pointer at each field offset in a given type.

      For a few reasons, including GC descriptors, the IL2CPP runtime code knows about the field layout of each type. We have some compiler specific code related to packing and field layout, but for the most part it is pretty straight-forward to understand how the C++ compiler will layout a given type.