IL2CPP, Unity's C# to C++ compiler, does not help for any of this. It just allows Unity to support platforms where JIT is not allowed or possible. The GC is the same if using Mono or IL2CPP. The performance of code is also roughly identical to Mono on average, which may be surprising, but if you inspect the generated code you'll see why [2].
[1] https://xoofx.github.io/blog/2018/04/06/porting-unity-to-cor... [2] https://www.jacksondunstan.com/articles/4702 (many good articles about IL2CPP on this site)
https://discussions.unity.com/t/coreclr-and-net-modernizatio...
C# it's plenty fast for game programming.
The developers of Risk of Rain 2 were undoubtedly aware of the hitches, but it interfered with their vision of the game, and affected users were left with a degraded experience.
It's worth mentioning that when game developers scope of the features of their game, available tech informs the feature-set. Faster languages thus enable a wider feature-set.
In this game's case though they possibly didn't do much optimization to reduce GC by pooling, etc. Unity has very good profiling tools to track down allocations built in so they could have easily found significant sources of GC allocations and reduced them. I work on one of the larger Unity games and we always profile and try to pool everything to reduce GC hitches.
This is true, but developer productivity also informs the feature set.
A game could support all possible features if written carefully in bare metal C. But it would take two decades to finish and the company would go out of business.
Game developers are always navigating the complex boundary around "How quickly can I ship the features I want with acceptable performance?"
Given that hardware is getting faster and human brains are not, I expect that over time higher level languages become a better fit for games. I think C# (and other statically typed GC languages) are a good balance right now between good enough runtime performance and better developer velocity than C++.
They probably create too much garbage. It’s equally easy to slow down C++ code with too many malloc/free functions called by the standard library collections and smart pointers.
The solution is the same for both languages: allocate memory in large blocks, implement object pools and/or arena allocators on top of these blocks.
Neither C++ nor C# standard libraries have much support for that design pattern. In both languages, it’s something programmers have to implement themselves. I did things like that multiple time in both languages. I found that, when necessary, it’s not terribly hard to implement that in either C++ or C#.
I think this is where the difference between these languages and rust shines - Rust seems to make these things explicit, C++/C# hides behind compiler warnings.
Some things you can't do as a result in Rust, but really if the rust community cares it could port those features (make an always stack type type, e.g.).
Code base velocity is important to consider in addition to dev velocity, if the code needs to be significantly altered to support a concept it swept under the rug e.g. object pools/memory arenas, then that feature is less likely to be used and harder to implement later on.
As you say, it's not hard to do or a difficult concept to grasp, once a dev knows about them, but making things explicit is why we use strongly typed languages in the first place...
GC can work or not when writing a game engine. However everybody who writes a significant graphical game engine in a GC language learns how to fight the garbage collector - at the very least delaying GC until between frames. Often they treat the game like safety critical: preallocate all buffers so that there is no garbage in the first place (or perhaps minimal garbage). Without garbage collection might technically use more CPU cycles, but in general they are spread out more over time and so more consistent.
You have to jump through some hoops but it's really not that convoluted and miles easier than good C++.
I wish there was an attribute in C# that was "[MustNotAllocate]" which files the compilation on known allocations such as these. It's otherwise very easy to accidentally introduce some tiny allocation into a hot loop, and it only manifests as a tiny pause after 20 minutes of runtime.
Even when allocations happen, .NET is much more tolerant to allocation traffic than, for example, Go. You can absolutely live with a few allocations here and there. If all you have are small transient allocations - it means that live object count will be very low, and all such allocations will die in Gen 0. In scenarios like these, it is uncommon to see infrequent sub-500us GC pauses.
Last but not least, .NET is continuously being improved - pretty much all standard library methods already allocate only what's necessary (which can mean nothing at all), and with each release everything that has room for optimization gets optimized further. .NET 9 comes with object stack allocation / escape analysis enabled by default, and .NET 10 will improve this further. Even without this, LINQ for example is well-behaved and can be used far more liberally than in the past.
It might sound surprising to many here but among all GC-based platforms, .NET gives you the most tools to manage the memory and control allocations. There is a learning curve to this, but you will find yourself fighting them much more rarely in performance-critical code than in alternatives.
That being said, .NET includes lots of performance-focused analyzers, directing you to faster and less-allocatey equivalents. There surely also is one on NuGet that could flag foreach over a class-based enumerator (or LINQ usage on a collection that can be foreach-ed allocation-free). If not, it's very easy to write and you get compiler and IDE warnings about the things you care about.
At work we use C# a lot and adding custom analyzers ensuring code patterns we prefer or require has been one of the best things we did this year, as everyone on the team requires a bit less institutional knowledge and just gets warnings when they do something wrong, perhaps even with a code fix to automatically fix the issue.
It's really not that hard to structure a game that pre-allocates and keeps per frame allocs at zero.
Unity used Mono. Which wasn't the best C# implementation, performance wise. After Mono changed its license, instead of paying for the license, Unity chose to implement their infamous IL2CPP, which wasn't better.
Now they want to use CoreCLR which is miles better than both Mono and IL2CPP.
Would be nice to hear about a Rust Game engine, though.
Also, if you invoke GC intentionally at convenient timing boundaries (I.e., after each frame), you may observe that the maximum delay is more controllable. Letting the runtime pick when to do GC is what usually burns people. Don't let the garbage pile up across 1000 frames. Take it out every chance you get.
Manually invoking GC many times per second is a viable approach?
You're basically trading off worse throughput for better latency.
If you forcibly run the GC every frame, it's going to burn cycles repeatedly analyzing the same still-alive objects over and over again. So the overall performance will suffer.
But it means that you don't have a big pile of garbage accumulating across many frames that will eventually cause a large pause when the GC runs and has to visit all of it.
For interactive software like games, it is often the right idea to sacrifice maximum overall efficiency for more predictable stable latency.
It might be more useful to use OSU! approach as a reference: https://github.com/dotnet/runtime/issues/96213#issuecomment-...
OSU! represents an extreme case where the main game loop runs at 1000hz, so for much more realistic ~120hz you have plenty of options.
Magic, code or otherwise, sucks when the spell/library/runtime has different expectations than your own.
You expect levitation to apply to people, but the runtime only levitates carbon based life forms. You end up levitating people without their affects (weapons/armor), to the embarrassment of everyone.
There should be no magic, everything should be parameterized, the GC is a dangerous call, but it should be exposed as well (and lots of dire warnings issued to those using it).
If you have a bunch of objects in an array that you have a reference to such that you can pass it, then, by definition, those objects are not garbage, since they're still accessible to the program.
There should be some middle ground between RAII and invoking Dispose/delete and full blown automatic GC.
The article discusses ref lifetime analysis that does have relationship with GC, but it does not force you into using one. Byrefs are very special - they can hold references to stack, to GC-owned memory and to unmanaged memory. You can get a pointer to device mapped memory and wrap it with a Span<T> and it will "just work".
AFAIK it has been possible to replace the GC with alternative implementation for the past few years, but no one has made one yet.
EDIT: Some experimental alternative GC implementations:
https://github.com/kkokosa/UpsilonGC
https://www.codeproject.com/Articles/5372791/Implementing-a-...
> Unity devs run into
So it's viable but not perfect
They also have a C# subset called Burst, which could have been avoided if they were using .NET Core.
BUT it's definitely not a language designed for no-gc so there are footguns everywhere - that's why Rider ships special static analysis tools that will warn you about this. So you can keep GC out of your critical paths, but it won't be pretty at that point. But better than Java :D
Possibly prettier than C and C++ still. Every time I write something and think "this could use C" and then I use C and then I remember why I was using C# for low-level implementation in the first place.
It's not as sophisticated and good of a choice as Rust, but it also offers "simpler" experience, and in my highly biased opinion pointers-based code with struct abstractions in C# are easier to reason about and compose than more rudimentary C way of doing it, and less error-prone and difficult to work with than C++. And building final product takes way less time because the tooling is so much friendlier.