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;
}
}