C# Coroutine WaitForSeconds Garbage Collection tip

It’s a mouthful of a title but it should be good for searches :slight_smile:

Ok, so I’m sure many veteran C#'ers know this already, but everywhere I look, when ever people are talking about Coroutines in C# I see this…

IEnumerator myAwesomeCoroutine()
{
	while (true)
	{
		doAwesomeStuff();
		yield return new WaitForSeconds(waitTime);
	}
}

What I wanted to point out was that using “yield return new WaitForSeconds()” causes a guaranteed 21 bytes of garbage allocation every time due to the “new” part (compared to the standard coroutine 9 bytes you would get with a “yield return null”).

To avoid this, simply set up your wait times in advance…

WaitForSeconds shortWait = new WaitForSeconds(0.1f);
WaitForSeconds longWait = new WaitForSeconds(5.0f);

IEnumerator myEvenAwesomerCoroutine()
{
	while (true)
	{
		if (iNeedToDoStuffFast)
		{
			doAwesomeStuffReallyFast();
			yield return shortWait;
		}
		else{
			dontDoMuch();
			yield return longWait;
		}
	}
}

Now your coroutine will only cause the bare minimum 9 bytes GC allocation each time it is called (not including other allocations you might be causing through your code of course!).

Those bytes all add up! :slight_smile:

60 Likes

I do the same.

However, someone who isn’t aware that caching objects, instead of constantly instantiating copies, is better for GC isn’t going to know to search about garbage collection tips for coroutines I’d bet =/

2 Likes

Could be right there, I was just assuming searching for “coroutine” or “garbage collection” might just put in in the list and be useful to someone who didn’t even realise they wanted to know it :slight_smile:

1 Like

If I reply to this it will be easy to find in future.

This seems like a good tip that I may implement

3 Likes

Yeah, I had a go at reusing the co-routine objects, and since it didn’t give me problems I wondered why it wasn’t the common solution.

1 Like

Are you guys often running into cases where dropping 10-15 bytes in these cases is a substantive performance issue?
Mobile?

The performance issue isn’t the allocation itself, it’s the garbage collection that allocations eventually trigger.

3 Likes

Thanks for the tip. I pool everything but for whatever reason I never thought about pooling WaitForSeconds

Ahh, I see, you are aiming to never have the GC happen at all (or nearly so)

3 Likes

Useful tip, thanks! Hadn’t thought about it, but it makes sense.

1 Like

The only problem I can foresee with using pre-instantiated WaitForSeconds objects is if you tried to use one in more than one coroutine at the same time. It may have a way to handle that case, but my guess is it would cause timing to get screwed up in both coroutines.

1 Like

The list of thing to be done to prevent garbage is rather long.

  • Don’t use Invoke or StartCoroutine with a string.
  • Don’t use GUILayout and flag your GUI MonoBehaviour to prevent the 800bytes/frames of GUI garbage from occurring. (Unity - Scripting API: MonoBehaviour.useGUILayout)
  • Don’t use GameObject.Tag or GameObject.Name
  • Never use GetComponent on Update, and cache it if possible
  • Don’t use foreach

etc… I think I have a page long of those somewhere.

5 Likes

You shouldn’t take it as a dogma though. There’s always a choice - frequent and quick garbage collections vs rare and long ones.

You can even cache them globally!
tested it with a few thousand objects sharing the YieldInstructions and worked flawlessly

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public static class Yielders {

    static Dictionary<float, WaitForSeconds> _timeInterval = new Dictionary<float, WaitForSeconds>(100);

    static WaitForEndOfFrame _endOfFrame = new WaitForEndOfFrame();
    public static WaitForEndOfFrame EndOfFrame {
        get{ return _endOfFrame;}
    }

    static WaitForFixedUpdate _fixedUpdate = new WaitForFixedUpdate();
    public static WaitForFixedUpdate FixedUpdate{
        get{ return _fixedUpdate; }
    }

    public static WaitForSeconds Get(float seconds){
        if(!_timeInterval.ContainsKey(seconds))
            _timeInterval.Add(seconds, new WaitForSeconds(seconds));
        return _timeInterval[seconds];
    }
   
}
33 Likes

Bumping for awesomeness. I just got rid of a bunch of un-necessary allocations using this.

2 Likes

Speaking of GC allocation and optimization, is it a big deal if there’s a small amount of allocations such as 4kb/frame? Is it necessary to put in the effort to optimize?

How about a ~50kb allocation maybe once every couple seconds?

1 Like

lol wtf this works?

scurries away to push buttons

2 Likes

As I could gather the YieldInstructions (WaitForSeconds, WaitForEndOfFrame and WaitForFixedUpdate) are handled by the coroutines just as a delimiter or conditional break. each coroutine must be handling internally the wait logic especially the elapsedTime (WaitForSeconds).

Therefore creating a new WaitForSeconds(1.0f) object at t=0 and then creating another new WaitForSeconds(1.0f) at t=1 causes the same effect as sharing the object in a cached value.
Hence they are not bound to the gameobject itself nor time of creation it is safe to share them between objects.

I have implemented it in my current project using coroutines for almost everything (AI loops, bullets, explosions, slow-mo effects, UI updates and such), the shared Yielders have not caused any bug or issue and have reduced the GC Alloc considerably.

@Crayz each shared/cached YieldInstruction will save just a few bytes (not a big deal), in the other hand creating them once they are needed will cause problems as Coroutines can be used heavily by multiple objects on multiple consequent frames and then released (once the coroutine has continued execution) the continuous allocation of YieldInstructions will trigger the Garbage Collector to clean the HEAP memory, THIS is the real issue here, GC is expensive and most of the time the only reason to have FPS drops or game update glitches.

2 Likes

I’d like to do this but before I do, where would this allocation appear in the profiler?

In the garbage collection columns?