Mastering C# and Unity3D

Iterators vs. Callbacks: Performance and Garbage

Iterator functions and their ability to yield return values then continue on really come in handy for a variety of situations. Unfortunately, they come with some pretty serious performance and garbage creation drawbacks. So today’s article explores alternatives in various forms of callbacks: delegates, interfaces, and classes. Can they perform better than iterator functions? Can they avoid garbage creation? Read on to find out!

In today’s test we’ll create a series of functions that all generate integers from zero to some maximum. The calling code will sum up these integers. This is a trivial case, but it’ll help isolate the performance and garbage creation concerns we really care about.

Let’s start by looking at the iterator functions and how we’d use them. First up is an iterator function returning IEnumerable<int>:

While the foreach loop had to go, the using block’s syntax sugar hiding try, finally, and Dispose is still saving us quite a bit of typing. Was this extra hassle worth it? We’ll see in the results below.

Next up is a function that returns values by a callback. The most natural way to do this in C# is to use a delegate such as Action<T>:

Notice that the yield return has given way to calling the Action<int> with each value. Otherwise the function is just as simple. The calling code is even shorter than the foreach version since there’s no longer an explicit loop. The lambda it passes in replaces the loop body.

Finally we’ll look at a pair of alternatives to delegates. First we’ll use an interface called IAction<T> with just a void Act(T param) function. We’ll also need to create a SumAction class implementing this interface:

One big advantage of this approach is that you can have an pool of these SumAction objects. This allows you to reuse them rather than letting the GC collect them, which isn’t really an option with an anonymous type created by a lambda or with iterator functions.

One downside of this approach is that the IAction<T> interface necessitates that the Act function be virtual. That’s slower than non-virtual functions, so let’s look at a tweaked version that uses a Summer class directly:

This version also allows object-pooling the Summer objects along with eliminating the virtual function call.

Now let’s look at how each of these perform and how much garbage they create. To do that I’ve created a multi-purpose test script that runs a single iteration when Unity’s profiler is enabled to check for garbage creation and a lot of iterations otherwise to check for performance. Have a look:

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.11.5

Unity 5.4.1f1, Mac OS X Standalone, x86_64, non-development

640×480, Fastest, Windowed

And here are the results I got:

Method

Time

Enumerator

71

Enumerable

73

Delegate

28

Interface

24

Class

22

Annoyingly, the more convenient the option the slower the performance. An IEnumerable<T> iterator was easiest to write and use, but it’s the slowest.

IEnumerator<T> isn’t much slower, but it does take a little more work to add the using block and an awkward while loop.

The callback-based function with a delegate eliminates the ability to pause and resume the function, which can be really handy, but gets a huge 2.5x speedup over the iterator functions.

The interface-based callback function requires you to create an interface, a class implementing it, and use an object pool to prevent garbage creation. That’s a lot of work for a pretty minor speedup!

Finally the non-interface, class-based callback function is an even smaller speedup as virtual functions aren’t really that slow in this use case.

Now let’s open up Unity’s profiler in “deep” mode and run the same script to see how much garbage is created:

Here we see that the iterator functions, either IEnumerable<T> or IEnumerator<T>, both result in 36 bytes of garbage creation. That’s not much, but many calls to the function will really add up and ultimately result in a huge delay on the main thread as Unity’s garbage collector kicks in.

The delegate-based callback is actually a lot worse! It results in 124 bytes of garbage creation, roughly 4x more than the iterator functions. If keeping garbage creation low is a priority, definitely don’t take this route!

Predictably, the interface- and class-based callback functions simulate object pooling by pre-allocating the callback object. This means that the GC will never clean up these objects as there will always be at least one reference to them. Consequently, both approaches don’t create any garbage at all.

As usual with engineering, there’s a tradeoff to be made here. If you want the easiest, most flexible, but slowest version, choose an IEnumerable<T>-based iterator function and loop over it with foreach. If you want the fastest, no-garbage, inflexible, hardest to use option, choose a callback-based function with either an interface- or class-based callback and utilize object pooling for those callback objects. Let me know which approach you end up using in your projects in the comments section!