Every time I see for (var i = 0; i < array.Length; ++i) I wonder if accessing that Length property is slow. Should I cache it? It's comforting to know that for (int i = 0, len = array.Length; i < len; ++i) is only dealing with local variables except on the first loop. Local variables must be faster, right? Likewise, I wonder the same thing about List<T>.Count. I finally got around to running a test to see if caching these length properties makes any performance difference. The answers might surprise you!

The test is dead simple: loop over a 100 million element byte[] and List<byte> with caching and no caching of the Length or Count property. Here are the four loops:

// Array.Length
for (var i = 0; i < array.Length; ++i)
for (int i = 0, len = array.Length; i < len; ++i)
 
// List.Count
for (var i = 0; i < list.Count; ++i)
for (int i = 0, len = list.Count; i < len; ++i)

But first, let's decompile the Array and List<T> classes to see how these properties are implemented in Unity's mscorlib.dll. What do they do? First, here's Array.Length:

public int Length
{
	[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
	get
	{
		int num = this.GetLength(0);
		for (int i = 1; i < this.Rank; i++)
		{
			num *= this.GetLength(i);
		}
		return num;
	}
}
 
[MethodImpl(MethodImplOptions.InternalCall)]
public extern int GetLength(int dimension);
 
public int Rank
{
	[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
	get
	{
		return this.GetRank();
	}
}
 
[MethodImpl(MethodImplOptions.InternalCall)]
private extern int GetRank();

So Length calls the native (C code) GetLength function. It also calls the Rank property which calls the native GetRank function, just in case this is a multi-dimensional array. This means that getting the Length property involves calling three C# functions and two C functions. Will that make Length expensive or is there some special handling for arrays that speeds this all up? We'll see in a minute when we performance test it.

Next is the decompilation of List<T>.Count from the same DLL:

public int Count
{
	get
	{
		return this._size;
	}
}

Wow! It literally just returns a private field! What more could we ask for in terms of performance from the Count property?

Without further ado, let's do the performance test:

using System.Collections.Generic;
 
using UnityEngine;
 
public class TestScript : MonoBehaviour
{
	void Start()
	{
		const int size = 100000000;
		var array = new byte[size];
		var list = new List<byte>(size);
		for (var i = 0; i < size; ++i)
		{
			list.Add(0);
		}
		var sw = new System.Diagnostics.Stopwatch();
 
		sw.Reset();
		sw.Start();
		for (var i = 0; i < array.Length; ++i)
		{
		}
		var arrayLengthTime = sw.ElapsedMilliseconds;
 
		sw.Reset();
		sw.Start();
		for (int i = 0, len = array.Length; i < len; ++i)
		{
		}
		var cachedArrayLengthTime = sw.ElapsedMilliseconds;
 
		sw.Reset();
		sw.Start();
		for (var i = 0; i < list.Count; ++i)
		{
		}
		var listCountTime = sw.ElapsedMilliseconds;
 
		sw.Reset();
		sw.Start();
		for (int i = 0, len = list.Count; i < len; ++i)
		{
		}
		var cachedListCountTime = sw.ElapsedMilliseconds;
 
		Debug.LogFormat(
			"Test,Property Time,Cached Property Time\n{0},{1},{2}\n{3},{4},{5}",
			"Array.Length", arrayLengthTime, cachedArrayLengthTime,
			"List.Count", listCountTime, cachedListCountTime
		);
	}
}

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 640x480 with fastest graphics. I ran it that way on this machine:

  • 2.3 Ghz Intel Core i7-3615QM
  • Mac OS X 10.11.5
  • Apple SSD SM256E, HFS+ format
  • Unity 5.4.0f3, Mac OS X Standalone, x86_64, non-development
  • 640x480, Fastest, Windowed

And here are the results I got:

Test Property Time Cached Property Time
Array.Length 33 31
List.Count 232 32

Cached Collection Length Property Performance Graph

I would have expected that Array.Length would be a lot slower given all of the function calls and complexity we saw in the decompilation. Clearly there's some special array handling that's eliminating all that work because the "cached" and "non-cached" times are pretty much the same. The cached version is about 10% faster, but the numbers are so close over so many iterations that the difference is trivial.

List<T>.Count is a whole other story. The cached property time is right in line with the array times, which makes sense since the loop is exactly the same for 99,999,999 of the 100 million loops. If you forget to cache Count it's a whole other story. Suddenly the loop takes 7.25x longer! All of that due to one trivial property getter function which isn't specially handled like Array.Length.

In conclusion, we have a simple takeaway. Don't worry about caching Array.Length but definitely cache List<T>.Count if you've got a long list to iterate over!

Got any tricks for arrays and lists? Post a comment!