Sign in to follow this  
Infinisearch

C# Garbage Collection and performance/stalls

Recommended Posts

Infinisearch    2971

Not to hijack the thread but one concern I always had with C# in regards to game programming is stalls due to garbage collection.  Is it really an issue?  Are there workarounds?  I mean general performance is most likely fine but if you have hiccups every now and again I would find it unacceptable.

Share this post


Link to post
Share on other sites
frob    44975

Splitting this off.  Please start a new thread rather than hijacking another.

 

For garbage collection, it CAN be an issue.  All memory management in games CAN be an issue.

 

There are many major games, including AAA games, that use C# either as a primary language or as a scripting language.  Intelligent policies about memory management go a long way.

Share this post


Link to post
Share on other sites
Nypyren    12074

Are there workarounds?


The main workaround is to minimize the conditions that cause the GC to trigger. Usually the GC runs when you pass certain thresholds in the total amount of allocated memory, or if something manually calls GC.Collect.

C# makes it easy (too easy?) to allocate things and then throw them away later - making a list in a function, returning it, then discarding it, for example. You want to try to minimize the amount of actual "garbage" you generate. Anything that you keep reusing will minimize the number of allocations you make, which in turn will give you more time between GCs.

This may occasionally cause you to make your code more cumbersome and bug-prone. Making new collections on the fly allows you to prove that no other code could be accessing that collection at the same time, which is 100% safe, but it leads to accumulating more garbage. Immutable data structures are good for parallelism, but making any changes requires allocations (and if the data structure is hierarchical, it can cause ridiculous amounts of allocations just to replace a single leaf element). Reusing a mutable collection instead eliminates some of this safety. You can pool reusable objects, but it means you need to verify that your pooling code doesn't have any bugs. Edited by Nypyren

Share this post


Link to post
Share on other sites
Infinisearch    2971


Splitting this off.  Please start a new thread rather than hijacking another.

Sorry it seemed related to his concerns with regards to performance so I figured it would complement his question.

 


The main workaround is to minimize the conditions that cause the GC to trigger. Usually the GC runs when you pass certain thresholds in the total amount of allocated memory, or if something manually calls GC.Collect.

But garbage collection when it happens is always proportional to the # of live objects right?  C# uses mark and prune right?  So basically when it is triggered if the data set of the program is large, collection will always cause a fairly large stall right?

Share this post


Link to post
Share on other sites
Nypyren    12074

But garbage collection when it happens is always proportional to the # of live objects right?  C# uses mark and prune right?  So basically when it is triggered if the data set of the program is large, collection will always cause a fairly large stall right?


Yes - the more objects you have allocated, the longer the mark step will take, even if none of them get collected. If you can minimize the total amount of objects you have allocated, that will help, but that directly limits the scope of your game. Minimizing the number of temporary allocations doesn't. Edited by Nypyren

Share this post


Link to post
Share on other sites
Bacterius    13165

Use value types like structs for temporary allocations, these are limited to their scope and are efficiently released when the program's flow exits their scope. Then you don't need to bother the GC with a million tiny 16-byte objects that are created and immediately destroyed every frame.

Share this post


Link to post
Share on other sites
Nypyren    12074
Bacterius is right; when structs are used properly, they are a great way to eliminate heap allocations.

structs as local variables, parameters, and return values are allocated on the stack just like they would be in C or C++. These are basically not going to impact the GC at all unless they're currently on the stack when the GC triggers (which will be so rare that it can be ignored).

structs as class members are allocated as part of the class itself, so they aren't treated as a separate allocation node that the GC has to traverse and mark separately (the struct's members CAN be references, though, which will need to be traversed by the GC like any other reference).

structs as the element type of arrays are allocated contiguously in the array, like they are in C or C++. This is also the case for collections that use arrays internally, such as List<T> and Dictionary<K,V> if the generic types are structs. If everything in an array is a value type, the *entire array* becomes a single traversal step for the GC. Edited by Nypyren

Share this post


Link to post
Share on other sites
Alberth    9529

But garbage collection when it happens is always proportional to the # of live objects right? C# uses mark and prune right? So basically when it is triggered if the data set of the program is large, collection will always cause a fairly large stall right?

The C# language specification does not contain details how the VM should do garbage collection. https://www.microsoft.com/en-us/download/details.aspx?id=7029

An implementation of a language is free to choose any algorithm that it likes. For this reason, you cannot say "C# does mark and prune" without explicitly stating what implementation you refer to.

You can also not draw conclusions about the language itself.
Languages have no speed, a language allows you to express how to perform a computation.
Like math, it can be compact, elegant, or long, but it's not fast or slow. It's just a static description of the computation.
It has no performance at all, until you get yourself an implementation of the language to compile and run it.

You can talk about performance of an implementation (compiler implementations, run-time system implementations) of a languages, but then you're discussing single instances, which can vary from instance to instance (C#.net and Mono can be very different), and from version to version.

Share this post


Link to post
Share on other sites
frob    44975
One nice thing about Mono is that you can choose which of several garbage collectors you use.


And about making garbage, there are different kinds of garbage based on the types of garbage collector used.

Compared with C++ and other systems, in those you generally clean up memory during destructors, and destructors are called explicitly when moving through the stack or cleaning up as part of work. In practice this means taking several moments during busy processing times. With GC systems it is possible -- if the system is used well -- for destructors and cleanup to run during idle times rather than run in the middle of your busy processing moments. Done well a GC system CAN improve your memory management performance, CAN be even more cache friendly than traditional heap allocators, CAN give benefits that in c++ require a carefully tuned multithreaded custom allocator.

Or, if you don't use it well, it CAN frequently trigger terrible nightmare scenarios, frequently stopping your entire application right at the moments you most need high performance, and CAN potentially stall for many milliseconds or even full seconds. This was originally the big fear and nightmare when Java gained popularity. You can use any tool badly.



With many of the common garbage collectors, what you really want is to first not make garbage, so make things on the stack if they belong on the stack. If you do create things on the heap, either make them extremely short lived (allocate, use, release immediately within in a function or as a return result) or make them longer lived (lasting across an entire play session or level or map).

Generally the GC can run quietly during idle moments in ways that you never notice. If your memory management is good the GC runs quietly as an idle-time thread that you never notice. GC systems are usually great at very short lived objects, since they can be passively cleaned up during idle times, and are great at very long lived objects since they can be quietly compacted during idle times. GC systems work well if your continuous allocations are kept to a minimum. Then the thread runs quietly in the background during your idle times. Since it is idle time processing rather than processing under load, it is even better performance than C++'s common designs.

Violate those rules, create tons of garbage so you consume your pools, or do lots of memory work continuously so you run faster than the idle-time background thread, or use intermediate-lifetime objects that require frequent compacting but then die soon after, and you'll quickly exceed the limits of your friendly idle-time garbage collector and trigger major 'stop the world' cleanups during your busy work times. That's when things get terrible.

Share this post


Link to post
Share on other sites
WozNZ    2010

Some people have mentioned that you use structs when possible to minimise heap impact.

 

If you do use structs in C# you should treat them like that value types they are and hence make them immutable. So readonly fields or get only properties and set these via a constructor.

 

There are a whole raft of subtle bugs that you can introduce into your code if you allow mutable structs all based around issues like, what are you changing, is it the copy you think it is or has it been boxed. Just far easier to avoid this and make immutable :)

Share this post


Link to post
Share on other sites
ericrrichards22    2434

Another technique that you can use if you have particular classes of heap objects that are allocated and deallocated often, and you want to avoid that triggering GC is to use objects pooling.  Basically you create a big List or array of default object instances at startup.  Then, when you want to create a new Foo object, instead of new'ing one up, you use a factory method on your object pool class, which finds an unused instance from the pre-allocated buffer, initializes it, and returns it to the caller.

 

Similar to good-old unmanaged memory, you would have to remember to return the instance to the pool when you are done with it.  If you've got control over the objects you are creating, you could make them implement IDisposable, and give them a delegate callback member, which would be supplied by the object pool manager's Create factory method, and called during Dispose(), to automate some of this.

Share this post


Link to post
Share on other sites
DvDmanDT    1941

My experience with C# on MS .NET says you can have short lived objects and long lived objects without much problems, what you'll want to avoid are the mid lifetime objects since those are the ones triggering the heavy collections.

 

With that said, I've had some great success optimizing performance, memory usage and responsiveness by using value types (structs) instead of reference types for bulk objects, even though they are sometimes a bit of a hassle to use.

 

A tip if you have Resharper is to download the Heap allocations plugin. It'll inform you when you are allocating stuff on the heap. For example, when calling methods with variable arguments, when you are using lambdas or when you are boxing value types. Most stuff will be temporary local objects that are basically free, but there are cases where it'll expose serious bottlenecks.

Share this post


Link to post
Share on other sites
WoopsASword    963

C# Garbage collection is problematic for big applications (Like office or web browsers) or servers.

For a game it could be an issue if you have poor object lifetime managment. 

Most of the resources are external and you dispose them by yourself (images, models, bindings, etc...)

 

There are plenty of information about the GC, you should read some articles or books about the performance issues with GC and understand how C# GC collects the objects.

+ There are much more to learn about .net performance. 

Share this post


Link to post
Share on other sites
Mona2000    1967

With Mono you can also run the GC in asynchronous mode and prevent major garbage collections from happening in the middle of your code (Mono.Runtime.SetGCAllowSynchronousMajor). It's no magic remedy, but it can help in games.

Share this post


Link to post
Share on other sites
Infinisearch    2971


You can also not draw conclusions about the language itself.

I didn't say anything about the language itself, although I would say that C# seems more dependent on it dynamic memory allocation system and that could impact performance.

 


Compared with C++ and other systems, in those you generally clean up memory during destructors, and destructors are called explicitly when moving through the stack or cleaning up as part of work. In practice this means taking several moments during busy processing times.

I don't know being spread out seems an easier problem to deal with than an all at once process.  In addition there is a chance that destructors will have relevent data primed in the cache.

 


Or, if you don't use it well, it CAN frequently trigger terrible nightmare scenarios, frequently stopping your entire application right at the moments you most need high performance, and CAN potentially stall for many milliseconds or even full seconds.

This was my fear, can you name any specific techniques to avoid such situations?  Besides the ones already mentioned?  Or any good articles to read.

 


There are a whole raft of subtle bugs that you can introduce into your code if you allow mutable structs all based around issues like, what are you changing, is it the copy you think it is or has it been boxed. Just far easier to avoid this and make immutable

I'll have to give this some thought.

 


Another technique that you can use if you have particular classes of heap objects that are allocated and deallocated often, and you want to avoid that triggering GC is to use objects pooling.  Basically you create a big List or array of default object instances at startup.  Then, when you want to create a new Foo object, instead of new'ing one up, you use a factory method on your object pool class, which finds an unused instance from the pre-allocated buffer, initializes it, and returns it to the caller.

You'd have to manually cleanup the data in unused pool members right?  So that GC doesn't keep alive data referenced by the now unused members.

 


There are plenty of information about the GC, you should read some articles or books about the performance issues with GC and understand how C# GC collects the objects.
+ There are much more to learn about .net performance. 

Have any articles for me?

Share this post


Link to post
Share on other sites
lwm    2518


My experience with C# on MS .NET says you can have short lived objects and long lived objects without much problems, what you'll want to avoid are the mid lifetime objects since those are the ones triggering the heavy collections.

 

This is one of the most important points in my experience.

Per-frame heap allocations are really not that bad if the newly created objects have short lifetimes and can be cleaned up by a generation 0 collection.

The majority of objects should either be created once at load-time and kept around "forever", or for a specific frame and only this specific frame.

Share this post


Link to post
Share on other sites
WoopsASword    963

 


You can also not draw conclusions about the language itself.

I didn't say anything about the language itself, although I would say that C# seems more dependent on it dynamic memory allocation system and that could impact performance.

 

 

 


Compared with C++ and other systems, in those you generally clean up memory during destructors, and destructors are called explicitly when moving through the stack or cleaning up as part of work. In practice this means taking several moments during busy processing times.

I don't know being spread out seems an easier problem to deal with than an all at once process.  In addition there is a chance that destructors will have relevent data primed in the cache.

 

 

 


Or, if you don't use it well, it CAN frequently trigger terrible nightmare scenarios, frequently stopping your entire application right at the moments you most need high performance, and CAN potentially stall for many milliseconds or even full seconds.

This was my fear, can you name any specific techniques to avoid such situations?  Besides the ones already mentioned?  Or any good articles to read.

 

 

 


There are a whole raft of subtle bugs that you can introduce into your code if you allow mutable structs all based around issues like, what are you changing, is it the copy you think it is or has it been boxed. Just far easier to avoid this and make immutable

I'll have to give this some thought.

 

 

 


Another technique that you can use if you have particular classes of heap objects that are allocated and deallocated often, and you want to avoid that triggering GC is to use objects pooling.  Basically you create a big List or array of default object instances at startup.  Then, when you want to create a new Foo object, instead of new'ing one up, you use a factory method on your object pool class, which finds an unused instance from the pre-allocated buffer, initializes it, and returns it to the caller.

You'd have to manually cleanup the data in unused pool members right?  So that GC doesn't keep alive data referenced by the now unused members.

 

 

 


There are plenty of information about the GC, you should read some articles or books about the performance issues with GC and understand how C# GC collects the objects.
+ There are much more to learn about .net performance. 

Have any articles for me?

 

 

https://msdn.microsoft.com/en-us/library/0xy59wtx(v=vs.110).aspx

 

Msdn is a good source for learning CLR and C#. 

If you have good knowledge about the C# Garbage collector, you could use some techniques to help you design objects to be GC friendly. 

Share this post


Link to post
Share on other sites
Alberth    9529

Msdn is a good source for learning CLR and C#.

If you have good knowledge about the C# Garbage collector, you could use some techniques to help you design objects to be GC friendly.

Too bad a different C# implementation may have different ideas on how to collect garbage, and break your tricks.

Share this post


Link to post
Share on other sites
ericrrichards22    2434

 

Msdn is a good source for learning CLR and C#.

If you have good knowledge about the C# Garbage collector, you could use some techniques to help you design objects to be GC friendly.

Too bad a different C# implementation may have different ideas on how to collect garbage, and break your tricks.

 

On that note, is anyone here privy to any details or rumblings about plans with Microsoft and Mono going forward?  With the CLR opening up, is it reasonable to start seeing some unification going on there, or is Microsoft going to do its own thing on Linux/Mac while Mono goes on its way?

Share this post


Link to post
Share on other sites
DvDmanDT    1941


On that note, is anyone here privy to any details or rumblings about plans with Microsoft and Mono going forward? With the CLR opening up, is it reasonable to start seeing some unification going on there, or is Microsoft going to do its own thing on Linux/Mac while Mono goes on its way?

 

Mono 4.x marks the start where Mono is incorporating code from Microsofts opensourced version. From what I understand, Microsoft will continue to do develop their own thing (.NET) and Mono will probably port most of that into its implementation. In other words, they'll remain separate projects but share some code and strive to complement each other for platforms etc. I'm also under the impression that they'll be takning steps towards a unified hosting API, but that's a vague personal interpretation based on non-official statements and mailing list posts.

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

Sign in to follow this