Unity’s GC is a continual thorn in our sides. We’re constantly working around it by pooling objects, limiting use of language features, and avoiding APIs. We even call GC.Collect on load screens in the hopes that the GC won’t run during gameplay. Today’s article goes one step further and shows how to disable the GC completely so there’s zero chance it’ll run. We’ll also see how to turn it back on when we’re ready for it again.

Unity’s GC is implemented in C code that ships with Unity. Here’s where it is in a Mac installation as of 2017.3:

/Applications/Unity/Unity.app/Contents/il2cpp/external/boehmgc/

In the misc.c file, we see these functions about 1800 lines down:

GC_API void GC_CALL GC_enable(void)
{
    DCL_LOCK_STATE;
 
    LOCK();
    GC_ASSERT(GC_dont_gc != 0); /* ensure no counter underflow */
    GC_dont_gc--;
    UNLOCK();
}
 
GC_API void GC_CALL GC_disable(void)
{
    DCL_LOCK_STATE;
    LOCK();
    GC_dont_gc++;
    UNLOCK();
}

These are very straightforward functions. They just increment an integer (GC_dont_gc) that serves as a global flag for whether the GC is enabled or not. If it’s zero, the GC is enabled. If it’s non-zero, the GC is disabled. So we should be able to call GC_disable to disable the GC and then GC_enable to re-enable it.

So let’s try calling these functions from C# using the [DllImport] attribute:

using System.Runtime.InteropServices;
 
public static class GcControl
{
	// Unity engine function to disable the GC
	[DllImport("__Internal")]
	public static extern void GC_disable();
 
	// Unity engine function to enable the GC
	[DllImport("__Internal")]
	public static extern void GC_enable();
}

Now we can call GcControl.GC_disable() or GcControl.GC_enable() from anywhere in the app to disable and re-enable the GC. It’s important to remember that these functions increment and decrement an integer in a manner similar to reference counting. Calls to GC_disable should therefore be matched with calls to GC_enable.

Now let’s try it out with a little app that displays the number of times the GC has run and a button to toggle the GC on and off. Every frame, we’ll allocate 100 KB of managed memory in a byte[] and hold onto it so the memory pressure just keeps increasing and causing the GC to run.

using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using UnityEngine;
using UnityEngine.UI;
 
class GCTestScript : MonoBehaviour
{
	// UI
	public Text gcCountText;
	public Text toggleButtonText;
 
	// GC status
	private bool isGcDisabled;
 
	// Holds allocated memory
	// Prevents GC from collecting it
	private List<byte[]> storage;
 
	void Awake()
	{
		storage = new List<byte[]>();
		toggleButtonText.text = "Disable GC";
	}
 
	void Update()
	{
		// Display how many times GC has run
		int gcCount = GC.CollectionCount(0);
		gcCountText.text = gcCount.ToString();
 
		// Add managed memory pressure
		storage.Add(new byte[100*1024]);
	}
 
	// Toggle GC on or off
	public void OnToggle()
	{
		isGcDisabled = !isGcDisabled;
		if (isGcDisabled)
		{
			GcControl.GC_disable();
			toggleButtonText.text = "Enable GC";
		}
		else
		{
			GcControl.GC_enable();
			toggleButtonText.text = "Disable GC";
		}
	}
 
	// Re-enable GC on exit
	void OnApplicationQuit()
	{
		if (isGcDisabled)
		{
			GcControl.GC_enable();
		}
	}
}

Here’s how it looks in action:

GC Disable Test App UI

Clicking the button to disable the GC does stop the GC collection count from rising. Clicking the button to re-enable the GC resumes the number’s rise as the GC kicks in to try to reclaim memory to make room for newly-allocated byte[] objects.

This works in both the Unity editor and on an Android device. There’s no guarantee that it’ll work on every single platform that Unity supports, but at least these two work.

So now that we have this tool at our disposal, how should we use it? To decide on a strategy, it’s important to understand the effects of the tool. Running the game with the GC disabled is new territory for most of us.

Normally, with the GC enabled, when we allocate managed memory such as by using new Class() and there isn’t enough room in the managed heap to store that object, two things happen. First, the GC is run to collect any garbage so the memory can be reused. Second, if there’s still not enough room, the managed heap is expanded to make room for the new object.

With the GC disabled, the first step won’t occur and there will be no possibility of reusing the memory of one or more garbage objects. Instead, we’ll proceed directly to step two and expand the managed heap. This means that memory usage will continue to grow as we allocate more and more objects. If we allocate enough, we may run out of memory and face consequences such as degraded performance due to using “virtual memory” (e.g. on Mac) or having our app terminated outright by the OS (e.g. on Android). Disabling the GC simply does not allow us to abandon all self-control and allocate as much as we want.

Since we really don’t want these things to happen, it’s vitally important that we don’t use too much memory while the GC is disabled. It’s often possible to arrange for this. For example, if we start out at 100 MB of memory usage after loading a level of the game and we know that we won’t allocate more than 100 MB of memory during the level, then we’re fine on any device where we can use up to 200 MB. As a safeguard, we can always query the memory usage periodically and re-enable the GC if we reach a threshold (e.g. 250 MB) that’s considered dangerous for the device.

It’s also worth noting that these C functions we’re calling are internal to the Unity engine, not part of the public API. There’s no guarantee that the function names, signatures, or behaviors will remain the same from version to version. Practically though, these functions are part of an off-the-shelf GC that Unity uses and the latest date in the file header is 2001, so it’s likely that this technique will keep working until Unity eventually replaces the GC. That’s currently on the roadmap under “Development” with the description “In-progress, timelines long or uncertain.”

Still, this is definitely an advanced technique and it should only be used with great caution. Disabling the GC for framerate-critical sections of the game and re-enabling it afterward (e.g. on a loading screen) may be a great benefit that’s worth it for some games, but it should be used very carefully.