Today’s article is the first to test Unity script performance speed. It establishes a way to set up and test C# scripts in Unity whether you have access to Pro or not. As a first example, I was reminded by the news this week that AddComponent(string) is being removed in Unity 5.0. These alternative versions of AddComponent and GetComponent aren’t something I normally use, but the news got me thinking of their performance compared to the generic-typed versions: GetComponent<ComponentType>(). The docs say to avoid the versions taking a string, but how bad could the performance really be? Today’s article puts the two versions to the test to find out just that!

Today’s test is very straightforward. It simply tests two versions of GetComponent:

// Generic type parameter version
otherGameObject.GetComponent<ComponentType>();
 
// String version
otherGameObject.GetComponent("ComponentType");

However, since this is the first Unity performance test I’ve done, a baseline procedure for testing needs to be established. To start, the excellent System.Diagnostics.Stopwatch class is my choice for measuring elapsed time. It’s easy to use and very precise. Here’s an example:

var stopwatch = new Stopwatch();
stopwatch.Start();
// ... run code to measure time for
stopwatch.Stop();
Debug.Log("code took " + stopwatch.EllapsedMilliseconds + " milliseconds");

Since I run tests more than once and don’t want to incur any GC overhead, a single Stopwatch is reused for each test. To reuse it, just call Reset before you use it again:

Stopwatch stopwatch;
 
void Start()
{
	stopwatch = new Stopwatch();
}
 
void Update()
{
	stopwatch.Reset();
	stopwatch.Start();
	// ... run code to measure time for
	stopwatch.Stop();
	Debug.Log("code took " + stopwatch.EllapsedMilliseconds + " milliseconds");
}

Another issue to work around is the incredible slowness of Debug.Log. Instead, let’s draw the results on the screen using GUI.Label:

void Update()
{
	stopwatch.Reset();
	stopwatch.Start();
	// ... run code to measure time for
	stopwatch.Stop();
	GUI.Label(
		new Rect(0, 0, Screen.width, Screen.height),
		"code took " + stopwatch.EllapsedMilliseconds + " milliseconds"
	);
}

The first parameter is the rectangle to draw in. We’ve sized it to the full screen so no text is clipped off. However, this new call is also causing some GC overhead. Let’s share it between calls:

Stopwatch stopwatch;
Rect drawRect;
 
void Start()
{
	stopwatch = new Stopwatch();
	drawRect = new Rect(0, 0, Screen.width, Screen.height);
}
 
void OnGUI()
{
	stopwatch.Reset();
	stopwatch.Start();
	// ... run code to measure time for
	stopwatch.Stop();
	GUI.Label(
		drawRect,
		"code took " + stopwatch.EllapsedMilliseconds + " milliseconds"
	);
}

Another issue is the string concatenation that’s happening to make the GUI.Label message. We can partially work around this by reducing the number of string objects allocated to just one per frame. The key is the System.Text.StringBuilder class. It maintains an internal buffer of characters that are used until you’re finished building your string. Simply use Append instead of the + operator:

// Build the string
var builder = new StringBuilder();
builder.Append("code took ");
builder.Append(stopwatch.EllapsedMilliseconds);
builder.Append(" milliseconds");
 
// Get the string we built
builder.ToString();

As long as the StringBuilder has enough Capacity in its internal buffer, no allocations will occur and there will be no GC overhead. However, we don’t want to allocate the StringBuilder every time we test because that would also cause GC overhead. We also want to preserve the internal buffer since it’s very likely to be sized well for the next test. The results shouldn’t change much between runs.

To reuse the StringBuilder, just set the Length field to zero and the previous string that was built will be overwritten:

StringBuilder stringBuilder;
 
void Start()
{
	stringBuilder = new StringBuilder();
}
 
void Update()
{
	// Build the string
	stringBuilder.Length = 0;
	stringBuilder.Append("code took ");
	stringBuilder.Append(stopwatch.EllapsedMilliseconds);
	stringBuilder.Append(" milliseconds");
 
	// Get the string we built
	builder.ToString();
}

Putting all these techniques together with the actual GetComponent calls, we end up with the final test script for today:

using System.Text;
 
using UnityEngine;
 
public class TestScript : MonoBehaviour
{
	private const int REPS = 100000;
 
	private GameObject otherGameObject;
	private System.Diagnostics.Stopwatch stopwatch;
	private Rect drawRect;
	private StringBuilder stringBuilder;
 
	void Start()
	{
		otherGameObject = new GameObject();
		otherGameObject.AddComponent<EventForwarder>();
 
		stopwatch = new System.Diagnostics.Stopwatch();
 
		drawRect = new Rect(0, 0, Screen.width, Screen.height);
		stringBuilder = new StringBuilder();
	}
 
	void OnGUI()
	{
		stringBuilder.Length = 0;
 
		stopwatch.Reset();
		stopwatch.Start();
		for (var i = 0; i < REPS; ++i)
		{
			otherGameObject.GetComponent<EventForwarder>();
		}
		stopwatch.Stop();
		stringBuilder.Append("Type Param: ");
		stringBuilder.Append(stopwatch.ElapsedMilliseconds);
		stringBuilder.Append('\n');
 
		stopwatch.Reset();
		stopwatch.Start();
		for (var i = 0; i < REPS; ++i)
		{
			otherGameObject.GetComponent("EventForwarder");
		}
		stopwatch.Stop();
		stringBuilder.Append("String: ");
		stringBuilder.Append(stopwatch.ElapsedMilliseconds);
		stringBuilder.Append('\n');
 
		GUI.Label(drawRect, stringBuilder.ToString());
	}
}

Simply create a new project and attach this script to its “Main Camera” GameObject.

The final issue is that code running in the Unity Editor is running in something of a debug or development mode. It’s roughly equivalent to the performance you can expect from a distributed game, but sometimes slower. For micro-benchmarks like these, it’s important to actually make a distribution build of the game. Luckily, that’s easy:

  1. File > Build Settings…
  2. Select “PC, Mac & Linux Standalone”
  3. Change “Architecture” to “x86_64”
  4. Make sure “Development Build” is not checked
  5. Click “Build”
  6. Enter a name for the app in the “Save As” field of the dialog
  7. Click “Save”

Build settings dialog

Build save dialog

Then run the app with as little graphical overhead as possible:

  1. Run the app
  2. Select “640 x 480” in the “Screen Resolution” list
  3. Check “Windowed”
  4. Set “Graphics Quality” to “Fastest”
  5. Click “Play!”

Run dialog

I tested this app using the following environment:

  • 2.3 Ghz Intel Core i7-3615QM
  • Mac OS X 10.10.1
  • Unity 4.6.1, Mac OS X Standalone, x86_64, non-development
  • 640×480, Fastest, Windowed

And got these results:

Version Time
Type Param 13
String 158

GetComponent Performance Graph

As you can see, there’s about a 12x performance boost to be had by switching from GetComponent(string) to GetComponent<ComponentType>(). Or you could look at it like a 12x slowdown when using the string version. It’s easy to see that you should strongly prefer to pass a generic type over a string and only fall back to a string when you have no other alternative.

That said, this test calls GetComponent 100,000 times per test. That means that the per-call times are 1/100,000 of the above numbers. Even the string version only takes .00158 milliseconds to complete, so it’s not going to slow down your app all on its own. At least as long as you aren’t calling it hundreds of times per frame with a string.

That wraps up today’s article. I’ll use the testing system outlined here today in future articles, so please let me know in the comments if you see any way to improve it!