Breaking the Rules of Unity Garbage Collection

Published January 01, 2020 by Ruben Torres Bonet (The Gamedev Guru)
Do you see issues with this article? Let us know.
Advertisement

[The post was originally posted here]

Once upon a time, there was a unity game programmer called Lancelot. A very passionate one, I would say. He didn't know yet, but eventually he would face the darkest side of Unity garbage collection.

(No time to read? Long story short version here)

Lancelot was always looking for bigger and bigger titles to work on. And so he worked hard to get his big chance in the games industry.

It was not easy, that he knew.

These spots in the game industry were and still are reserved for a small minority of game programmers. And he wasn't sure he'd be up to the standards, anyway.

But he persisted and continued to sharpen his programming skills.

Lancelot started working on small games. Maybe at some point, he would get the big chance he was looking for, he thought.

Years passed till he got the opportunity he was waiting for. He was asked to port a big VR game to a mobile platform. As excited as Lancelot was, he couldn't stop wondering if he was good enough for the task. It was daunting but he accepted the challenge. He knew that he could only grow out of that.

Lancelot's biggest concern was the need to massively improve the performance of the game. It was a double challenge, in fact. He had to improve performance by 20% for a considerably less powerful platform.

After months of non-stop work, he finally managed to optimize the game enough to have a rock-solid performance baseline.

However, an unexpected issue spawned just around the corner.

The unity profiler revealed him a considerable frame-rate drop every few seconds. And that worried him because that wouldn't allow him to ship the game. The game release was in jeopardy. That made him utterly uncomfortable.

Based on his previous experience, Lancelot quickly suspected of the garbage collector. After all, he knew that allocating temporary memory in the game too often could cause these performance spikes. Just like the trash can in everyone's kitchen, you know it's time to clean it when it reaches 80% of its capacity.

And so he spent days fighting the bothersome memory allocations. He performed all types of optimizations he could think of. Object pools, data caching, data structure optimizations...

Spending these days optimizing got him a bit ahead in the performance journey. Lancelot was proud of his work, but his concerns only grew when he saw the garbage collector still running every 15 seconds.

Playing the game in VR with these performance drops would somehow make people look pale.

"How could that be happening?" he wondered.

With more patience and digging, Lancelot discovered a second source of memory allocations he didn't see before. Those happened in a 3rd party library.

He had a look and soon realized he was in the worst position ever: that library was closed source. Not only that, he also tried using Unity's incremental garbage collector but he couldn't afford to pay its performance price.

Lancelot was running out of options.

He felt desperate but managed to keep calm. He's been in worse situations after all.

He could reverse engineer the library and do the memory allocation optimizations himself. The problem was that the license disallowed such things. And he was too young to go to jail.

The second option he considered was to pre-allocate a lot of memory on the heap. He knew that unity triggered the garbage collection process when the heap usage reached a certain percentage. So increasing the heap should give him more time between garbage collections.

Sadly, that was still not enough.

It slowly felt as if he had no control over the situation. It was tough, but again, he persisted.

So Lancelot came up with a crazy idea. What if he disabled the garbage collection entirely? Was that even possible? He felt in his bones how risky such an idea was. He didn't want to add the possibility for the game to crash at unpredictable points. Last time he checked, that was no fun for the players. Maybe times did change, but better safe than sorry.

On top of that, he worried about delaying the release of the game. He didn't want his players to miss this title for Christmas. He remembered how much fun he had playing EverQuest during these holidays. He wouldn't take that away from the players.

Reached this point, he had no other choice than to disable the garbage collector.

He got into research mode and found that he could indeed manually disable the garbage collection. He ran dozens of experiments to see how long the game would hold without running out of memory. He did all sorts of tests to stress the system. Clicking everywhere, walking and jumping around, switching between different applications.

Numbers started arriving in his spreadsheet: 25 minutes, 28 minutes, 30 minutes... He also noted how the heap usage increased over time to be certain he'd never exceed a safe budget.

With those numbers, Lancelot established a generous safe margin and prepared a prototype. He would run the garbage collection manually during loading screens and every few minutes.

He had hope again.

He politely asked QA to go through the game dozens of times.

Memory was always within the budget. No crashes. No side effects.

This long journey brought him to the point where he was able to ship the game.

And guess what? Hundreds of players are now enjoying it over the Christmas period.

At the beginning, he was not comfortable with this solution. It was a risky move and he knew it. But he managed to pull it off.

Lancelot learned to be comfortable with the uncomfortable. He learned to be more pragmatic. Because there are times when a programmer has to be.

Does anything of the story ring a bell? If so, your intuition is probably right.

That programmer was me.

For the times you need it, this is how you can manage the garbage collector:

public class GarbageCollectionManager : MonoBehaviour
{
  [SerializeField] private float maxTimeBetweenGarbageCollections = 60f;
  private float _timeSinceLastGarbageCollection;
  private void Start(){
#if !UNITY_EDITOR
    GarbageCollector.GCMode = GarbageCollector.Mode.Disabled;
#endif// You might want to run this during loading times, screen fades and such.// Events.OnScreenFade += CollectGarbage;
  }
  private void Update(){
    _timeSinceLastGarbageCollection += Time.unscaledDeltaTime;
    if (_timeSinceLastGarbageCollection > maxTimeBetweenGarbageCollections)
    {
      CollectGarbage();
    }
  }
  private void CollectGarbage(){
    _timeSinceLastGarbageCollection = 0f;
    Debug.Log("Collecting garbage"); // talking about garbage... 
#if !UNITY_EDITOR// Not supported on the editorc
    GarbageCollector.GCMode = GarbageCollector.Mode.Enabled;
    GC.Collect();
    GarbageCollector.GCMode = GarbageCollector.Mode.Disabled;
#endif
  }
}

That code snippet shows you how to disable automatic garbage collections. It runs the GC process manually every minute and possibly during screen transitions (fade to black).

Be aware of its possible side effects:

  • Crashes: if you don't play safe enough you'll run out of memory. Worse, the OS might kill your game when you switch between applications
  • Longer garbage collection times: increasing the heap will make future garbage collections slower

If you need to produce generous amounts of garbage, here's a straightforward method that'll just work:

public class GenerousGarbageCreator : MonoBehaviour
{
  [SerializeField] private int garbageCreationRate = 1024;
  private static int[] _garbage;
  void Update()
  {
    _garbage = new int[garbageCreationRate];
  }
}

This is what you'll be getting in the profiler:

Unity Garbage Collection: Time-Based Manual Trigger

Unity Garbage Collection: Time-Based Manual Trigger

There you see an increasing memory usage. The growing heap usage is highlighted as "mono". Luckily for us, we're running the manual garbage collector every 3 seconds.

You can clearly see this garbage generation-clearance cycle in the profiler graph. For those game developers who studied physics, you might recognize it as a sawtooth wave shape.

For more general memory optimizations, you might be interested in Unity Addressables. With addressables you get to reduce your total memory usage so you can trigger garbage collections less frequently. In turn, this will reduce the performance spikes your players will experience.

If you want the source code of this project, you know where to find it (spoiler: here).

I'm looking forward to working with you all in 2020. Happy new year.

Ruben



Cancel Save
0 Likes 0 Comments

Comments

Nobody has left a comment. You can be the first!
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!

Featured Tutorial

What happens when memory allocations go out of hands, are out of your control and you have to ship a game?

Advertisement
Advertisement