Foreach Performance Considerations in DotNet (C#)

Posted in software by Christopher R. Wirz on Fri Jun 02 2017

The .Net System.Collections.Generic offers the IEnumerable<T> interface for collections. IEnumerable is the base interface for all non-generic collections that can be enumerated. This interface has the method GetEnumerator() that returns an enumerator, allowing the code to traverse the collection - and is agnostic of the underlying structure (e.g., trees, linked lists or arrays).

There are several ways to access items in collections. For Lists and Arrays (the most common collections), there is for, foreach, and the direct use of the enumerator. Each choice will have several performance implications.
Let's try a few approaches to implementation that will demonstrate the differences:


/// <summary>
/// Runs a test of each Loop given a number of items
/// </summary>
/// <param name="numberOfItems">The number of items</param>
/// <returns>Returns an array of results</returns>
public static long[] Run(int numberOfItems)
{
    var ret = new long[6];
    var sw = new System.Diagnostics.Stopwatch();
    var testList = MakeTestList(numberOfItems);

    // for loop on List
    var current = 0;
    sw.Start();
    for (int i = 0; i < numberOfItems; i++)
    {
        current += testList[i];
    }
    sw.Stop();
    ret[0] = sw.Elapsed.Ticks;

    // foreach loop on List
    current = 0;
    sw.Restart();
    foreach (var val in testList)
    {
        current += val;
    }
    sw.Stop();
    ret[1] = sw.Elapsed.Ticks;

    // enumerator MoveNext on List
    current = 0;
    sw.Restart();
    var enumerator = testList.GetEnumerator();
    while (enumerator.MoveNext())
    {
        current += enumerator.Current;
    }
    sw.Stop();
    ret[2] = sw.Elapsed.Ticks;

    var testArray = MakeTestArray(numberOfItems);
    current = 0;
    // for loop on Array
    sw.Restart();
    for (int i = 0; i < numberOfItems; i++)
    {
        current += testArray[i];
    }
    sw.Stop();
    ret[3] = sw.Elapsed.Ticks;

    current = 0;
    // foreach loop on Array
    sw.Restart();
    foreach (var val in testArray)
    {
        current += val;
    }
    sw.Stop();
    ret[4] = sw.Elapsed.Ticks;

    // enumerator MoveNext on Array
    current = 0;
    sw.Restart();
    var aenumerator = testArray.GetEnumerator();
    while (aenumerator.MoveNext())
    {
        current += (int)aenumerator.Current;
    }
    sw.Stop();
    ret[5] = sw.Elapsed.Ticks;

    return ret;
}

Here is the average of 30 runs (all times in Ticks):



Average Time (in ticks) to access N number of items

itemsfor (List)foreach (List)enumerator (List)for (Array)foreach (Array)enumerator (Array)
12.033333333333332.61.666666666666661.033333333333331.533333333333334.875
22.066666666666663.8751.908333333333331.741666666666661.533333333333338.19166666666666
42.333333333333333.7583333333333321.708333333333331.49.50833333333333
82.908333333333333.808333333333332.458333333333331.533333333333331.810.8108108108108
162.63.5752.71.91.5333333333333319.9916666666666
324.055.14.216666666666662.833333333333332.136.3070175438596
646.608333333333337.017241379310347.44.608333333333333.766.3333333333333
1289.4285714285714212.008403361344510.87179487179486.441666666666665.40833333333333127.88990825688
25617.141666666666621.566666666666622.62511.6759.33333333333333275.35294117647
51233.166666666666640.92544.016666666666623.516949152542318.8728813559322571.294117647058
102459.171171171171176.114035087719384.491228070175444.407079646017734.5044247787611097.51304347826
2048116.936936936936149.619469026548166.31858407079687.223214285714267.57657657657652147.59821428571
4096231.930434782608294.592920353982340.059322033898176.235294117647133.1428571428574104.08928571428
8192461.061403508771589.48623853211657.990990990991348.587719298245266.6454545454548479.42056074766
16384961.4615384615381205.848214285711341.31578947368690.747747747747534.8090909090917569.896226415
327681920.116071428572431.660714285712725.704347826081406.078947368421159.2941176470533939.8392857142
655363695.321428571424955.177966101695466.715517241372854.842105263152132.5327102803768994.5963302752
1310727472.252173913049789.3534482758610718.02631578945674.31254328.04504504504139963.706422018
26214414987.091743119219296.559633027521547.196428571411189.88570.91964285714280734.256637168
52428830614.944954128440569.873949579843130.570093457923046.565217391317339.6788990825560014.281818181
104857660923.853211009178384.672566371687335.361111111145209.363636363635261.8751111569.1875
2097152126156.403508771158404.578947368175643.03571428591761.408695652170533.752243857.67826086


Let's normalize the times to ticks per item, shall we?



Normalized Average Time (with respect to ticks / item)

itemsfor (List)foreach (List)enumerator (List)for (Array)foreach (Array)enumerator (Array)
40.5083333333333330.650.4166666666666650.2583333333333330.3833333333333331.21875
80.2583333333333330.4843750.2385416666666660.2177083333333330.1916666666666661.02395833333333
160.1458333333333330.2348958333333330.1250.1067708333333330.08750.594270833333333
320.09088541666666660.1190104166666670.07682291666666660.04791666666666660.056250.337837837837837
640.0406250.0558593750.04218750.02968750.02395833333333330.312369791666666
1280.0316406250.039843750.03294270833333330.02213541666666660.016406250.283648574561403
2560.02581380208333330.0274110991379310.028906250.01800130208333330.0144531250.259114583333333
5120.01841517857142860.0234539128151260.02123397435897420.01258138020833330.01056315104166670.249784977064219
10240.01673990885416660.02106119791666660.02209472656250.01140136718750.009114583333333330.268899356617646
20480.01619466145833330.019982910156250.02149251302083330.01148288532838980.009215274099576270.278952205882353
40960.01444608671171170.01858252809758770.02062774122807020.01084157217920350.00842393183075220.267947520380435
81920.01427452843468460.01826409534015480.02030256153207960.01064736502511160.008249093820382870.262157985142299
163840.01415591032608690.01798052492394910.0207555738546080.01075654871323530.008126395089285710.250493730817522
327680.01407047740200110.01798969233801610.02008029147311370.01063805295709980.008137373490767030.258771379417348
655360.01467073880709130.01839978354317790.02046685469777950.01053997417827980.008160539106889190.268095340368881
1310720.01464932305472240.01855209895542690.02079547384510860.01072753103155840.00884471220128670.258940424237932
2621440.01409653254917690.01890250383797340.02085386473557040.01089035837273850.00813496669876240.263193497964001
5242880.01425218996794330.01867170991568730.02044301283986930.01082289218902590.008255090799417570.266959584087406
10485760.0142928044730370.01840263331701990.0205490078244890.01067142486572270.008173865931374680.267729050290268
20971520.01459834335047170.01934522340277660.02056625847504520.01098945866460390.008268203210393190.267035618695345
41943040.0145253785159610.01868836225661550.0208223727014330.01077875224026770.008407086133956910.265018746256828
83886080.01503901523456230.01888329731790640.02093828150204240.01093881233878760.008408278226852420.267488679678543


Visually, this might make more sense on a log2 - log2 plot...





These results are really interesting. First we see that for and foreach loops on Arrays are fastest. This should stand to reason as arrays use sequential memory layout - but here is the strange part: calling the MoveNext() on the array is by far the slowest (like 36x slower). Using the enumerator's MoveNext() on the List is slower than calling foreach, but not by much. The moral of the story is not to use the enumerator with MoveNext() as it is always slower.

The difference between the Array's enumerator vs the List's enumerator is surprising. How is that possible? Let's look at the source code...


// List implementation
public bool MoveNext()
{
	List<T> list = _list;
	if (_version == list._version && (uint)_index < (uint)list._size)
	{
		_current = list._items[_index];
		_index++;
		return true;
	}
	if (_version != _list._version)
	{
		ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion();
	}
	_index = _list._size + 1;
	_current = default(T);
	return false;
}



// Array implementation
public bool MoveNext()
{
	if (_complete)
	{
		index = endIndex;
		return false;
	}
	index++;
	int rank = array.Rank;
	_indices[rank - 1]++;
	for (int num = rank - 1; num >= 0; num--)
	{
		if (_indices[num] > array.GetUpperBound(num))
		{
			if (num == 0)
			{
				_complete = true;
				break;
			}
			for (int i = num; i < rank; i++)
			{
				_indices[i] = array.GetLowerBound(i);
			}
			_indices[num - 1]++;
		}
	}
	return !_complete;
}

Does this difference explain the 16x difference in speed? Sure. .Net's Array allows for partitioning of the Array across several allocations of memory - which makes sense for large data sets where there might not be a continuous allocation available in that size. A list doesn't have this consideration as each element does not need to be adjacent in memory - so there is only one case to handle. This additional complexity causes overhead in the enumerator.