The Path to CoreCLR #1: The Problem

The Problem

Prologue

I’m the “It’s happening!” meme right now.

No. Really.

The release of the CoreCLR scripting backend has been a long time coming, years in the making. Having it finally go out the door comes with a swirl of emotions spanning from nervousness to exhilaration, with a pit stop at relief in between. Transitioning the entire engine from Mono to CoreCLR was a massive, sprawling and technically complicated task that involved pretty much every engineer who works on the Unity Engine. At this point I know I share this feeling with many of you:

It’s. Finally. Happening.

So in the brief respite between initial release and diving back in to continue to improve the engine, it’s worth taking a step back. It was pointed out to me that it would be nice to have a series of educational blog posts discussing some of the more interesting problems we solved and how we solved them. In the spirit of education… and because developers genuinely enjoy telling war stories about a difficult problem they beat, the deep dive technical blog posts are also happening.

This first one is about the problem itself. Why Mono had to go, what CoreCLR gives us, and why swapping CoreCLR turned into years of work. Since this is an introduction to the series I’ll do my best to establish the setting without getting too deep into the weeds. In the following posts our developers will be performing deeper technical dives into their respective topics.

The Mono Runtime

As many of you know, the crux of the entire effort has been to sunset the Mono runtime. It’s gotten old, frail, and frankly many of our users have outgrown its once substantial capabilities. Before moving onto all of Mono’s weaknesses though, I want to bend your ear for a moment to reminisce how important Mono has been to Unity.

At the end of the day, something needs to take all that C# that we write and turn it into something that our computers can execute. For Unity’s case we currently have four options to make this happen: Mono, IL2CPP, Burst and now CoreCLR. Back in the day though we only had Mono. It was chosen for several reasons.

First, it could be run embedded within a native application. It’s somewhat odd to think about since all of our user code is written in C# but Unity is essentially a native application written in C++. Mono’s native embedding API allowed Unity to load, start, and most importantly interact with a C# runtime that ran within Unity’s memory allocation. This allowed engine developers to do interesting things like push expensive to run code down to C++ where it could be optimized.

Second, Mono is also cross-platform. Microsoft’s .NET Framework C# runtime in the beginning was Windows only, whereas Mono could run on every operating system where Unity wanted to be.

Third, Mono supported .NET Framework’s AppDomain capability. This was crucial to Unity’s signature feature of iterating on code without needing to build and launch a separate player over and over. While enter/exit playmode would eventually become the source of much pain, it was a huge boon to our users for many years. The ability of Mono to tear down an AppDomain, load and re-JIT an updated one for use in the same runtime was a huge advantage.

Finally, Mono used Boehm-Demers-Weiser as its garbage collector. Casually referred to as Boehm or BDWGC, it is a conservative, non-moving collector. The first two of these properties mattered to Unity in their own way. A conservative GC assumes that any pointer sized value, managed or native, could be a live reference to an object. This allows Unity’s C++ code to manipulate managed objects without needing to register everything with the GC. Boehm would still scan and find them. A non-moving GC is one that, once it allocates an object, doesn’t relocate it on the heap. That was extremely useful because it allowed significant portions of Unity’s native code to assume that an object would stay in place in memory until its death. It significantly reduced the amount of bookkeeping that needed to be done under the hood.

Mono Aging Gracefully

It all had to come to an end eventually. Technology moved on, games grew more advanced and more demanding of the engines that they were built on. Mono was the little engine that could for years until IL2CPP came along and supplied a liferaft to the performance and platform support issues that were already appearing.

There are several points in history that I could potentially point at as “the last straw” that prompted the move to CoreCLR. The one I’d personally pick: Microsoft acquired Xamarin in 2016, becoming Mono’s steward. A few years later, when Mono was re-targeted as an alternative runtime for .NET 5 and moved into the dotnet/runtime repository, it had to conform to .NET Core’s API surface, which has no multiple domain support. AppDomains had been ruled out of .NET Core years earlier. Legacy Mono kept them to the end, but the writing was on the wall. The feature Unity had built play mode on had no future in the runtime everyone else was moving to.

That isn’t to say there weren’t other chinks in the armor that helped things along.

Boehm, the GC that had many attributes that made Unity want to select Mono in the first place also suffered from the weaknesses of its chosen design. The conservative, non-moving nature meant that for long-lived processes the allocated heap would become fragmented, leading to large memory footprints where considerable portions were effectively unused.

Mono’s JIT, impressive for its extensibility and compilation speed, simply could not keep pace with CoreCLR’s RyuJIT. Additionally, any fixes made to Mono’s JIT in the upstream repository required significant efforts to backport to Unity’s fork. Mono had officially become tech debt by every definition of the term.

With the end of official support for Mono that left its C# API surface limited to .NET Framework 4.7 plus .NET Standard 2.1. The latter being extremely important because it is what provided the bridge that needed to be crossed to transition all our existing C# code from Mono to CoreCLR.

What CoreCLR Gives Us

CoreCLR, the modern and supported C# runtime provided by Microsoft today is a significant step forward in time. Literally. We are leaving the era of effectively abandoned 2019 technology and leaping forward in time to .NET 10.

A Modern GC

Arguably the biggest game changer is CoreCLR’s GC which is a precise, generational, compacting GC.

A precise GC knows exactly where all managed allocates can be stored. This is what allows it to move and compact the GC heap. The catch here is that a precise GC only knows about managed allocations, leaving native objects invisible to it. Keep that in mind, because it comes back to bite us later in this post.

A generational GC is, roughly, one that separates allocations by how old they are. The general idea being that the longer an object lives the more likely it is to be a long-lived object and the less that the GC needs to check on it to see if it needs to be freed. This allows the GC to not have to scan the entire managed heap on every marking pass. More about CLR generational GC can be found here: Garbage Collection Fundamentals.

Lastly, a compacting GC is one that will move surviving objects in memory, preventing the memory fragmentation I mentioned earlier.

A Modern JIT

CoreCLR’s RyuJIT brings tiered compilation to the Unity landscape along with intrinsics support. Unlike Mono’s JIT, RyuJIT covers both fast compilation and optimized output, which is where the performance gains already being touted with the CoreCLR player come from.

A Modern C# API Surface

Microsoft’s .NET team has not been sitting around during Unity’s journey with Mono. They’ve been making performance improvements, adding new performant APIs (System.Text.Json), and we will be getting these by default through the transition to CoreCLR.

Better Diagnostics

CoreCLR brings proper mixed-mode debugging that is supported natively by standard .Net debuggers. Modern profilers can be used and the entire dotnet-trace, dotnet-counters, dotnet-dump set can be used instead of Mono’s custom tooling.

Active Upstream Support

Microsoft is still actively working on and improving its implementation of C#. When Unity updates to the latest version from upstream we get all this without significant internal investment.

Okay… So Just Swap CoreCLR In?

So finally I’ve gotten to why I’ve titled this post “The Problem”. You were probably thinking I was referring to Mono, and I was, but not entirely.

Transitioning Unity to a new scripting backend conceptually sounds easy right? We already did it once when we brought IL2CPP online. Just drop in CoreCLR, rename a few dll loads and everything’s good.

Not quite.

Limited embedding API. It can be hosted, it started life as Silverlight’s runtime, but its hosting API is narrow in scope. Mono’s embedding API gave us the keys to the kingdom and we’ve come to expect that level of access. We needed to restore that access by creating our own embedding layer.

The GC moves things now. CoreCLR brings along with it its awesome, efficient GC. That’s great! Now remember how I mentioned that a significant amount of Unity’s engine code was written on the assumption that managed objects would not move in memory? Yeah that’s a problem.

Playmode lost its foundation. AppDomains, the C# feature that Unity had based playmode on, doesn’t exist in CoreCLR. We needed to find another solution as removing playmode from the editor wasn’t really an option.

Our profiling tooling was built on Mono. Unity’s excellent internal profiling tooling relies heavily on Mono’s native embedding APIs. Many of them being ones that we’ve added ourselves to in our fork of Mono. All of that had to be built again for CoreCLR.

IL2CPP needed to be updated. IL2CPP is our gateway to many of the platforms that Unity supports. The transition to CoreCLR brought along with it a new class library implementation that leverages modern APIs. IL2CPP needed a significant overhaul to get it caught up to speed.

Performance by Default

I’ll be honest. Unity’s swinging for the fences here. We’ve managed to take a portion of the codebase that is generally invisible when it’s doing its job right and made it a tentpole achievement for Unity 7. That comes with expectations to meet. CoreCLR is a significant improvement over Mono in many ways, but as always, there is no magic bullet. In bringing it online we rewrote significant portions of the engine code where we hedged towards stability over speed. In order to deliver a product that we are proud of, what everyone expects of us, we need to prioritize Performance by Default.

Waiting for the editor to reload after a code change should not prompt a trip to fetch coffee.

Opening a project cold shouldn’t be an overnight affair.

Having the editor crash during a deep debugging session should not be expected.

Micromanaging memory via obscure tribal-knowledge tricks should no longer be required.

And plenty more.

Over the coming months each of the challenges above will be given its own in-depth technical post. I hope you are looking forward to them as much as I am.

It’s happening!

63 Likes

Is this something that should be marked as Official?

3 Likes

Probably not necessary. I think this more about the devs geeking out about the cool stuff they’ve been doing and less about stuff you actually need to know in order to use Unity or understand its future.

Thanks for the writeup! Cool to see some behind the scenes and goals

1 Like

This is a super interesting retrospective and a good insight in why CoreCLR took as long as it did.

Crazy how Mono was the correct choice at the time, but steadily became the wrong choice. No criticism there, just interesting how software develops over time.

6 Likes

Yeah, it’s finally happening! I’ve been waiting for this for years.

It’s amazing to see how much Unity has evolved; from GameObjects to DOTS, and now ECS for All and CoreCLR. I’ve learned so much throughout this journey, and I’m really excited to read the upcoming technical posts.

Thank you!

As much as I’ve dreamed about this over the past decade, in the last year or so I’ve also started to develop a bit of dread for it too. In years past I wouldn’t have fretted it much but lately… let’s just say that I hope CoreCLR doesn’t also become the “wrong choice” some day. EEE might not be an openly spoken mantra at Microsoft anymore but it is still very much an active mindset.

I’m actually most interested in the micromanaging memory tricks based on obscure tribal-knowledge (aka cargo cult). It probably involves calling UnloadUnusedResources consecutively over precisely 7 frames (unless it’s Halloween, then it should be 13) and things like that. :laughing:

See you say that but there is a function that’s suspiciously close… Boehm known for its ever expanding memory footprint is able to return memory it has reserved back to the operating system. The catch is that block of memory needs to survive a set number of consecutive marking passes (GC collections) without any allocations in that block of memory before it does so. I believe this value is defaulted to 6.

Micromanaging that particular behavior isn’t terribly useful however as in most scenarios Boehm is going to immediately ask for that block back if you are already running near capacity.

Either way there’s some fun, mostly useless knowledge for you :slight_smile:

5 Likes

Ah, I see …

GC.Collect();
GC.Collect();
GC.Collect();
GC.Collect();
GC.Collect();
GC.Collect();
GC.Collect();

That about right? :grinning_face_with_smiling_eyes:

1 Like

Spot on, actually.

Quite literally what has been suggested by Unity devs themselves in the past on these very forums - uh, with the caveat that you probably shouldn’t be doing it anyway because there’s better ways to handle stuff.

1 Like

Yeah that is terrible advice actually.

  1. Boehm only fully returns that memory to the OS on Windows
  2. It might only take 1 or 3 calls since it’s GC-Colllect-Cycles-Since-Startup % 6 == 0 so you’re better off checking the profiler stats for the reserved GC heap number and stop earlier when that number declines
  3. That number declines on all platforms but everything but Windows immediately remaps it (as untracked by the Memory Profiler) with mprotect or madvice to keep the address space reserved
  4. Calling the GC that often can take a while
  5. If you keep that code around for CoreCLR this will promote objects to higher lifetime buckets
  6. This probably makes fragmentation issues worse rather than provide improved memory utilization

So overall it just tends to be better to leave that up to the automatic GC.Collect triggers.

3 Likes

CoreCLR’s default GC is not made for games, and especially the compacting part is a mistake. NOT A FEATURE.
Of course, better GC is needed, but a specialized one. None of the current dotnet GC really give good design for gaming applications.
Compacting brings tons of the problems too. Not only objects were not moved in BDWGC, but they were also guaranteed to not be deallocated midframe when incremental GC was used.

In my opinion games won’t ever need more than the Gen 0 allocations at run time, so compaction could happen just between main game switches (return to main menu whatever). Imo it can work.

Just don’t allocate garbage lol :recycling_symbol:

2 Likes

It’s hard to specialize something like that when you give as much freedom as you do to create objects in all kinds of shapes and sizes when in GO land. ECS on the other hand gives exactly that sort of specialization with the caveat that you are going to have to play by some pretty strict rules.

1 Like

That’s not quite true. If you allocate enough in middle of the frame to trigger GC, it can and will collect objects mid frame.

Yes, I agree, but ONLY in controllable manner. The thing with default dotnet GC is that it’s not.

Well, yes, but with zero allocation strategies it’s fine. Especially with Unity’s allocated components etc, this will probably be never collected while the object is still alive since engine holds managed handle inside native engine Object impl.