Take the 2-minute tour ×
Game Development Stack Exchange is a question and answer site for professional and independent game developers. It's 100% free, no registration required.

*As far as I know, Unity3D for iOS is based on the Mono runtime and Mono has only generational mark & sweep GC.

This GC system can't avoid GC time which stops game system. Instance pooling can reduce this but not completely, because we can't control instantiation happens in the CLR's base class library. Those hidden small and frequent instances will raise un-deterministic GC time eventually. Forcing complete GC periodically will degrade performance greatly (can Mono force complete GC, actually?)

So, how can I avoid this GC time when using Unity3D without huge performance degrade?

share|improve this question
2  
The latest Unity Pro Profiler includes how much garbage is generated in certain function calls. You can use this tool to help see other places that might be generating garbage that might not be immediately obvious otherwise. –  Tetrad Mar 12 '12 at 15:05

2 Answers 2

up vote 15 down vote accepted

Luckily, as you pointed out, the COMPACT Mono builds use a generational GC (in stark contrast to the Microsoft ones, like WinMo/WinPhone/XBox, who just maintain a flat list).

If your game is simple the GC should handle it just fine, but here are some pointers you might want to look into.

Premature Optimization

First make sure this is actually a problem for you before trying to fix it.

Pooling Expensive Reference Types

You should pool reference types that you either create often, or which have deep structures. An example of each would be:

  • Created often: A Bullet object in a bullet-hell game.
  • Deep structure: Decision tree for an AI implementation.

You should use a Stack as your pool (unlike most implementations which use a Queue). The reason for this is because with a Stack if you return an object to the pool and something else immediately grabs it; it will have a much higher chance of being in an active page - or even in the CPU cache if you are lucky. It's just that tiny bit faster. Furthermore always size-limit your pools (just disregard 'checkins' if your limit has been exceeded).

Avoid Creating New Lists to Clear Them

Don't create a new List when you actually meant to Clear() it. You can re-use the backend array and save a load of array allocations and copies. Similarly to this try and create lists with a meaningful initial capacity (remember, this isn't a limit - just a starting capacity) - it doesn't need to be accurate, just an estimate. This should apply to basically any collection type - except for a LinkedList.

Use Struct Arrays (or Lists) Where Possible

You gain little benefit from use structs (or value types in general) if you pass them around between objects. For example, in most 'good' particle systems the individual particles are stored in a massive array: the array and and index are passed around instead of the particle itself. The reason this works so well is because the when the GC needs to collect the array it can skip the contents entirely (it's a primitive array - nothing to do here). So instead of looking at 10 000 objects the GC simply needs to look at 1 array: huge gain! Again, this will only work with value types.

After RoyT. provided some viable and constructive feedback I feel I need to expand on this more. You should only use this technique when you are dealing with massive amounts of entities (thousands to tens of thousands). In addition it must be a struct which must not have any reference type fields and must live in an explicitly-typed array. Contrary to his feedback we are placing it in an array which is very likely a field in a class - meaning that it is going to land up the heap (we are not trying to avoid a heap allocation - merely avoiding GC work). We really care about is the fact that it's a contiguous chunk of memory with a lot of values that the GC can simply look at in a O(1) operation instead of a O(n) operation.

You should also allocate these arrays as close to your application startup as possible to mitigate the chances of fragmentation occurring, or excessive work as the GC tries to move these chunks around, (and consider using a hybrid linked list instead of the built-in List type).

GC.Collect()

This is definitely THE BEST way to shoot yourself in the foot (see: "Performance Considerations") with a generational GC. You should only call it when you have created an EXTREME amount of garbage - and the one instance where that could be an issue is just after you have loaded the content for a level - and even then you should probably only collect the first generation (GC.Collect(0);) to hopefully prevent promoting objects to the third generation.

IDisposable and Field Nulling

It is worthwhile to null fields when you no longer need an object (more-so on constrained objects). The reason is in the details of how the GC works: it only removes objects that are not rooted (i.e. referenced) even if that object would have been unrooted because of other objects being removed in the current collection (note: this depends on the GC flavour in use - some do actually clean up chains). In addition, if an object survives a collection it is immediately promoted to the next generation - this means that any objects left lying around in fields will be promoted during a collection. Each successive generation is exponentially more expensive to collect (and occurs as infrequently).

Take the following example:

MyObject (G1) -> MyNestedObject (G1) -> MyFurtherNestedObject (G1)
// G1 Collection
MyNestObject (G2) -> MyFurtherNestedObject (G2)
// G2 Collection
MyFurtherNestedObject (G3)

If MyFurtherNestedObject contained a multi-megabyte object you can be guaranteed the GC won't look at it for quite a long time - because you have inadvertently promoted it to G3. Contrast that with this example:

MyObject (G1) -> MyNestedObject (G1) -> MyFurtherNestedObject (G1)
// Dispose
MyObject (G1)
MyNestedObject (G1)
MyFurtherNestedObject (G1)
// G1 Collection

The Disposer pattern helps you set up a predictable way to ask objects to clear their private fields. For example:

public class MyClass : IDisposable
{
    private MyNestedType _nested;

    // A finalizer is only needed IF YOU CONTROL UNMANAGED RESOURCES
    // ~MyClass() { }

    public void Dispose()
    {
       _nested = null;
    }
}
share|improve this answer
1  
WTF are you on about? The .NET Framework on Windows is absolutely generational. Their class even has GetGeneration methods. –  DeadMG Mar 12 '12 at 16:16
2  
.NET on Windows uses a Generational Garbage Collector, however the .NET Compact Framework as used on WP7 and the Xbox360 have a more limited GC, altough it's probably more than a flat list it's not a full generational one. –  Roy T. Mar 12 '12 at 16:25
    
Jonathan Dickinson: I would like to add that structs are not always the answer. In .Net and so probably also Mono a struct does not necessarily live on the stack (which is good) but can also live on the heap (which is where you need a Garbage Collector). The rules for this are fairly complicated but in general it happens when a struct contains non-value types. –  Roy T. Mar 12 '12 at 16:30
1  
@RoyT. see above comment. I indicated that structs are good for a large liner amount of data in an array - like a particle system. If you are worried about the GC structs in an array are the way to go; for data like particles. –  Jonathan Dickinson Mar 12 '12 at 22:01
7  
@DeadMG also WTF is highly uncalled for - considering you actually didn't read the answer correctly. Please calm your temper and re-read the answer before writing hateful comments in a generally well-behaved community like this. –  Jonathan Dickinson Mar 12 '12 at 22:11

Very little dynamic instantiation happens automatically inside the base library, unless you call something that requires it. The vast majority of memory allocation and deallocation comes from your own code and you can control that.

As I understand it, you can't avoid this entirely - all you can do is ensure that you recycle and pool objects where possible, Use structs instead of classes, avoid the UnityGUI system, and generally avoid making new objects in dynamic memory during times when performance matters.

You can also force garbage collection at certain times, which may or may not help: see some of the suggestions here: http://unity3d.com/support/documentation/Manual/iphone-Optimizing-Scripts.html

share|improve this answer
2  
You should really explain the implications of forcing GC. It plays havoc with the GC heuristics and layout and can lead to bigger memory problems if used incorrectly: the XNA/MVPs/staff community generally considers post-loading-content the only reasonable time to force a collect. You should really avoid mentioning it entirely if you only dedicate one sentence to the matter. GC.Collect() = free memory now but very likely problems later. –  Jonathan Dickinson Mar 12 '12 at 14:55
    
No, you should explain the implications of forcing GC because you know more about it than me. :) However, bear in mind that the Mono GC is not necessarily the same as the Microsoft GC, and the risks may not be the same. –  Kylotan Mar 12 '12 at 15:01
    
Still, +1. I went ahead and elaborated on some personal experience with the GC - which you might find interesting. –  Jonathan Dickinson Mar 12 '12 at 15:34

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.