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>:

private IEnumerable<int> Enumerable(int numReps)
{
	for (var i = 0; i < numReps; ++i)
	{
		yield return i;
	}
}
 
var sum = 0;
foreach (var i in Enumerable(numReps))
{
	sum += i;
}

This one’s really simple. It’s a textbook iterator function utilizing yield return to give the caller one value at a time. The caller simply uses a foreach loop and inherits some pretty nice syntax sugar.

How nice? The next iterator function returns IEnumerator<T> which means that foreach won’t work with it. While the iterator function itself is almost identical, the calling code gets more complex:

private IEnumerator<int> Enumerator(int numReps)
{
	for (var i = 0; i < numReps; ++i)
	{
		yield return i;
	}
}
 
var sum = 0;
using (var enumerator = Enumerator(numReps))
{
	while (enumerator.MoveNext())
	{
		sum += enumerator.Current;
	}
}

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>:

private void Delegate(int numReps, Action<int> callback)
{
	for (var i = 0; i < numReps; ++i)
	{
		callback(i);
	}
}
 
var sum = 0;
Delegate(numReps, i => sum += i);

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:

interface IAction<T>
{
	void Act(T param);
}
 
class SumAction : IAction<int>
{
	public int Sum;
 
	public void Act(int param)
	{
		Sum += param;
	}
}
 
private void Interface(int numReps, IAction<int> callback)
{
	for (var i = 0; i < numReps; ++i)
	{
		callback.Act(i);
	}
}
 
var sumAction = new SumAction();
Interface(numReps, sumAction);
// sumAction.Sum is the result

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:

class Summer
{
	public int Sum;
 
	public void Add(int param)
	{
		Sum += param;
	}
}
 
private void Class(int numReps, Summer summer)
{
	for (var i = 0; i < numReps; ++i)
	{
		summer.Add(i);
	}
}
 
var summer = new Summer();
Class(numReps, summer);
// summer.Sum is the result

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:

using System;
using System.Collections.Generic;
 
using UnityEngine;
 
interface IAction<T>
{
	void Act(T param);
}
 
class SumAction : IAction<int>
{
	public int Sum;
 
	public void Act(int param)
	{
		Sum += param;
	}
}
 
class Summer
{
	public int Sum;
 
	public void Add(int param)
	{
		Sum += param;
	}
}
 
class TestScript : MonoBehaviour
{
	private SumAction sumAction;
	private Summer summer;
 
	void Start()
	{
		sumAction = new SumAction();
		summer = new Summer();
 
		if (Profiler.enabled)
		{
			TestGarbage();
		}
		else
		{
			TestPerformance();
		}
	}
 
	private void TestPerformance()
	{
		const int NumReps = 10000000;
 
		var stopwatch = new System.Diagnostics.Stopwatch();
 
		stopwatch.Reset();
		stopwatch.Start();
		TestEnumerator(NumReps);
		var enumeratorTime = stopwatch.ElapsedMilliseconds;
 
		stopwatch.Reset();
		stopwatch.Start();
		TestEnumerable(NumReps);
		var enumerableTime = stopwatch.ElapsedMilliseconds;
 
		stopwatch.Reset();
		stopwatch.Start();
		TestDelegate(NumReps);
		var delegateTime = stopwatch.ElapsedMilliseconds;
 
		stopwatch.Reset();
		stopwatch.Start();
		TestInterface(NumReps);
		var interfaceTime = stopwatch.ElapsedMilliseconds;
 
		stopwatch.Reset();
		stopwatch.Start();
		TestClass(NumReps);
		var classTime = stopwatch.ElapsedMilliseconds;
 
		Debug.Log(
			"Method,Time\n"
			+ "Enumerator," + enumeratorTime + "\n"
			+ "Enumerable," + enumerableTime + "\n"
			+ "Delegate," + delegateTime + "\n"
			+ "Interface," + interfaceTime + "\n"
			+ "Class," + classTime
		);
	}
 
	private void TestGarbage()
	{
		const int NumReps = 1;
 
		TestEnumerator(NumReps);
		TestEnumerable(NumReps);
		TestDelegate(NumReps);
		TestInterface(NumReps);
		TestClass(NumReps);
	}
 
	private int TestEnumerator(int numReps)
	{
		var sum = 0;
		using (var enumerator = Enumerator(numReps))
		{
			while (enumerator.MoveNext())
			{
				sum += enumerator.Current;
			}
		}
		return sum;
	}
 
	private int TestEnumerable(int numReps)
	{
		var sum = 0;
		foreach (var i in Enumerable(numReps))
		{
			sum += i;
		}
		return sum;
	}
 
	private int TestDelegate(int numReps)
	{
		var sum = 0;
		Delegate(numReps, i => sum += i);
		return sum;
	}
 
	private int TestInterface(int numReps)
	{
		Interface(numReps, sumAction);
		return sumAction.Sum;
	}
 
	private int TestClass(int numReps)
	{
		Class(numReps, summer);
		return summer.Sum;
	}
 
	private IEnumerator<int> Enumerator(int numReps)
	{
		for (var i = 0; i < numReps; ++i)
		{
			yield return i;
		}
	}
 
	private IEnumerable<int> Enumerable(int numReps)
	{
		for (var i = 0; i < numReps; ++i)
		{
			yield return i;
		}
	}
 
	private void Delegate(int numReps, Action<int> callback)
	{
		for (var i = 0; i < numReps; ++i)
		{
			callback(i);
		}
	}
 
	private void Interface(int numReps, IAction<int> callback)
	{
		for (var i = 0; i < numReps; ++i)
		{
			callback.Act(i);
		}
	}
 
	private void Class(int numReps, Summer summer)
	{
		for (var i = 0; i < numReps; ++i)
		{
			summer.Add(i);
		}
	}
}

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

Iterators vs. Callbacks Performance Graph

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:

Iterators vs. Callbacks Garbage Creation

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!