Unity Coroutine Performance
Unity’s coroutine support allows you to easily create pseudo-threads and write synchronous-looking code that doesn’t block the rest of the app. They can be very handy for a variety of tasks. Before using them, we should understand the performance cost. Today’s article takes a look at the cost of starting a coroutine as well as the cost of running it. Just how expensive are they? Read on to find out!
Coroutines are just C# iterator functions. That means they return a System.Collections.IEnumerator
and have at least one yield return X
statement in them. Here’s one that moves a GameObject
toward a destination every time it’s resumed:
IEnumerator MoveToDestination( GameObject objectToMove, Vector3 destination, float speed ) { // Not at destination yet while (objectToMove.transform.position != destination) { // Move toward destination objectToMove.transform.position = Vector3.MoveTowards( objectToMove.transform.position, destination, Time.deltaTime * speed ); // Yield new position yield return objectToMove.transform.position; } }
You could manually iterate over this function, but Unity can do that for you by starting it as a coroutine. If you do, it’ll be resumed every frame just like your Update
function. To start it as a coroutine, all you need is a MonoBehaviour
and to call the StartCoroutine function on it like so:
class MyScript : MonoBehaviour { void Start() { // Pass the IEnumerator the coroutine function returns // to the StartCoroutine function StartCoroutine(MoveToDestination(gameObject, Vector3.zero, 5)); } IEnumerator MoveToDestination( GameObject objectToMove, Vector3 destination, float speed ) { // ... } }
Now to test its performance. I’ve set up a small script that starts one thousand, ten thousand, or one hundred thousand coroutines that do nothing but yield. They’re the cheapest coroutine you could write. They also start up as fast as possible, which is good because I’m measuring the time it takes to start up all the coroutines. From there on I display the frame rate the app is running at. Here’s the code:
using System; using System.Diagnostics; using System.Reflection; using UnityEngine; using UnityEngine.UI; using System.Collections; public static class StopwatchExtensions { public delegate void TestFunction(); public static long RunTest(this Stopwatch stopwatch, TestFunction testFunction) { stopwatch.Reset(); stopwatch.Start(); testFunction(); return stopwatch.ElapsedMilliseconds; } } public class TestScript : MonoBehaviour { private Rect drawRect; private bool showModeScreen; private long startTime; private const float UpdateInterval = 1; private float totalTime; private int numFrames; private float timeleft; private float fps; void Start() { drawRect = new Rect(0, 0, Screen.width, Screen.height); showModeScreen = true; } void OnGUI() { if (showModeScreen) { GUI.Label(new Rect(0, 0, 200, 25), "How many coroutines?"); if (GUI.Button(new Rect(0, 25, 100, 25), "1,000")) { StartTest(1000); } else if (GUI.Button(new Rect(0, 50, 100, 25), "10,000")) { StartTest(10000); } else if (GUI.Button(new Rect(0, 75, 100, 25), "100,000")) { StartTest(100000); } } else { timeleft -= Time.deltaTime; totalTime += Time.timeScale / Time.deltaTime; numFrames++; if (timeleft <= 0) { fps = totalTime / numFrames; timeleft = UpdateInterval; totalTime = 0; numFrames = 0; } GUI.Label(drawRect, "Start Time: " + startTime + ", FPS: " + fps); } } private void StartTest(int numCoroutines) { showModeScreen = false; var stopwatch = new Stopwatch(); startTime = stopwatch.RunTest( () => { for (var i = 0; i < numCoroutines; ++i) { StartCoroutine(CoroutineFunction()); } } ); } private IEnumerator CoroutineFunction() { do { yield return null; } while (true); } }
If you want to try out the test yourself, simply paste the above code into a TestScript.cs
file in your Unity project’s Assets
directory and attach it to the main camera game object in a new, empty project. Then build in non-development mode for 64-bit processors and run it windowed at 640×480 with fastest graphics. I ran it that way on this machine:
- 2.3 Ghz Intel Core i7-3615QM
- Mac OS X 10.10.2
- Unity 4.6.3, Mac OS X Standalone, x86_64, non-development
- 640×480, Fastest, Windowed
And got these results:
Number | Start Time | FPS |
---|---|---|
1000 | 6 | 650 |
10000 | 75 | 121 |
100000 | 814 | 8.2 |
First off, starting coroutines is cheap. It’s not free either, but unless you’re suddenly starting hundreds of them in a single frame it probably won’t matter. In cases where you’re tempted to do that, like making a bunch of game objects all move to a destination at the exact same time, you should probably consider spreading that work out over multiple frames or not using coroutines.
As for the actual coroutine performance, we see here that it’s certainly possible to drive the frame rate into the ground with nothing but empty coroutines. In this sense, the overhead is enough to make them expensive. On the other hand, a hundred thousand coroutines is probably not a realistic number. But even 1000 is much higher than the test computer’s frame rate with no coroutines: about 740. There is definitely some expense to them above and beyond simple function calls.
So if you’re looking for maximum performance, coroutines aren’t for you. However, they are really quite cheap when used in small numbers. A few dozen shouldn’t be much of an issue for most games.
Do you have any thoughts to add on coroutines in Unity? Love ’em? Hate ’em? Had performance troubles? Post a comment!
#1 by Shawn Blais Skinner on May 19th, 2015 ·
The one issue I just ran into with co-routines, was Garbage Collection. I didn’t realize that everytime you cann StartCoroutine, it allocated 9bytes of memory. And each time you do something like yield return new WaitForSeconds(.1f) you’re generating even more garbage.
Given that, there’s some best practices. For example, avoid calling StartCoroutine if you can, so prefer
To:
Or use internal timers to monitor time, rather than WaitForSeconds(), so prefer:
To:
#2 by jackson on May 19th, 2015 ·
It makes sense that some memory would be allocated behind the scenes from a
StartCoroutine
call, but it’s good to have the exact byte figure. I haven’t yet run into a case where I needed to start a lot of them at once, so this hasn’t been an issue for me. How has the 9 byte allocation impacted your projects?As for the
WaitForSeconds
class, have you considered reusing instances of it? I never have, but if your wait times are constant then it seems like that may be a viable alternative.#3 by Jarnak on June 17th, 2015 ·
Quite interesting, thank you for this post!
#4 by Leucaruth on July 4th, 2015 ·
I just discovered your site while searching for coroutines performance. Thanks for all your hard work. Its really useful to have these kind of unity references so I just suscribed. Please, keep the good work :)
#5 by jackson on July 5th, 2015 ·
Glad to hear you’re enjoying the site! Let me know—in comments or mail—if there’s anything in particular you’d like to see articles about.
#6 by Idea++ on March 2nd, 2016 ·
Thanks for the article. There is a bug in the test. OnGUI() may be called multiple times in the same frame. Therefore the resulting Start Time and FPS values are unreliable. Use Time.frameCount to check if it’s in the same frame, if the time bookkeeping has to be in OnGUI().
#7 by jackson on March 2nd, 2016 ·
Thanks for pointing this out! You’re correct that
OnGUI
can be called more than once per frame, leading to inaccuracies. I adjusted it to only count ifTime.frameCount
had advanced and re-ran the test using the same computer but Unity 5.3.2f1. Here are the results I got:The second test now hits the 60 FPS cap. I adjusted it to 50,000 coroutines and got about 16 FPS, just as a data point.
The conclusion remains the same though: coroutines are cheap and probably not an issue for most games unless you start running thousands of them.
Thanks again for pointing out the issue!
#8 by bzor on April 14th, 2016 ·
people might be interested in this: https://www.assetstore.unity3d.com/en/#!/content/54975
it’s helped me squeeze some perf out where I was using a lot of coroutines
#9 by Luc on November 16th, 2021 ·
Broken link :-(