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
items | for (List) | foreach (List) | enumerator (List) | for (Array) | foreach (Array) | enumerator (Array) |
---|---|---|---|---|---|---|
1 | 2.03333333333333 | 2.6 | 1.66666666666666 | 1.03333333333333 | 1.53333333333333 | 4.875 |
2 | 2.06666666666666 | 3.875 | 1.90833333333333 | 1.74166666666666 | 1.53333333333333 | 8.19166666666666 |
4 | 2.33333333333333 | 3.75833333333333 | 2 | 1.70833333333333 | 1.4 | 9.50833333333333 |
8 | 2.90833333333333 | 3.80833333333333 | 2.45833333333333 | 1.53333333333333 | 1.8 | 10.8108108108108 |
16 | 2.6 | 3.575 | 2.7 | 1.9 | 1.53333333333333 | 19.9916666666666 |
32 | 4.05 | 5.1 | 4.21666666666666 | 2.83333333333333 | 2.1 | 36.3070175438596 |
64 | 6.60833333333333 | 7.01724137931034 | 7.4 | 4.60833333333333 | 3.7 | 66.3333333333333 |
128 | 9.42857142857142 | 12.0084033613445 | 10.8717948717948 | 6.44166666666666 | 5.40833333333333 | 127.88990825688 |
256 | 17.1416666666666 | 21.5666666666666 | 22.625 | 11.675 | 9.33333333333333 | 275.35294117647 |
512 | 33.1666666666666 | 40.925 | 44.0166666666666 | 23.5169491525423 | 18.8728813559322 | 571.294117647058 |
1024 | 59.1711711711711 | 76.1140350877193 | 84.4912280701754 | 44.4070796460177 | 34.504424778761 | 1097.51304347826 |
2048 | 116.936936936936 | 149.619469026548 | 166.318584070796 | 87.2232142857142 | 67.5765765765765 | 2147.59821428571 |
4096 | 231.930434782608 | 294.592920353982 | 340.059322033898 | 176.235294117647 | 133.142857142857 | 4104.08928571428 |
8192 | 461.061403508771 | 589.48623853211 | 657.990990990991 | 348.587719298245 | 266.645454545454 | 8479.42056074766 |
16384 | 961.461538461538 | 1205.84821428571 | 1341.31578947368 | 690.747747747747 | 534.80909090909 | 17569.896226415 |
32768 | 1920.11607142857 | 2431.66071428571 | 2725.70434782608 | 1406.07894736842 | 1159.29411764705 | 33939.8392857142 |
65536 | 3695.32142857142 | 4955.17796610169 | 5466.71551724137 | 2854.84210526315 | 2132.53271028037 | 68994.5963302752 |
131072 | 7472.25217391304 | 9789.35344827586 | 10718.0263157894 | 5674.3125 | 4328.04504504504 | 139963.706422018 |
262144 | 14987.0917431192 | 19296.5596330275 | 21547.1964285714 | 11189.8 | 8570.91964285714 | 280734.256637168 |
524288 | 30614.9449541284 | 40569.8739495798 | 43130.5700934579 | 23046.5652173913 | 17339.6788990825 | 560014.281818181 |
1048576 | 60923.8532110091 | 78384.6725663716 | 87335.3611111111 | 45209.3636363636 | 35261.875 | 1111569.1875 |
2097152 | 126156.403508771 | 158404.578947368 | 175643.035714285 | 91761.4086956521 | 70533.75 | 2243857.67826086 |
Let's normalize the times to ticks per item, shall we?
Normalized Average Time (with respect to ticks / item)
items | for (List) | foreach (List) | enumerator (List) | for (Array) | foreach (Array) | enumerator (Array) |
---|---|---|---|---|---|---|
4 | 0.508333333333333 | 0.65 | 0.416666666666665 | 0.258333333333333 | 0.383333333333333 | 1.21875 |
8 | 0.258333333333333 | 0.484375 | 0.238541666666666 | 0.217708333333333 | 0.191666666666666 | 1.02395833333333 |
16 | 0.145833333333333 | 0.234895833333333 | 0.125 | 0.106770833333333 | 0.0875 | 0.594270833333333 |
32 | 0.0908854166666666 | 0.119010416666667 | 0.0768229166666666 | 0.0479166666666666 | 0.05625 | 0.337837837837837 |
64 | 0.040625 | 0.055859375 | 0.0421875 | 0.0296875 | 0.0239583333333333 | 0.312369791666666 |
128 | 0.031640625 | 0.03984375 | 0.0329427083333333 | 0.0221354166666666 | 0.01640625 | 0.283648574561403 |
256 | 0.0258138020833333 | 0.027411099137931 | 0.02890625 | 0.0180013020833333 | 0.014453125 | 0.259114583333333 |
512 | 0.0184151785714286 | 0.023453912815126 | 0.0212339743589742 | 0.0125813802083333 | 0.0105631510416667 | 0.249784977064219 |
1024 | 0.0167399088541666 | 0.0210611979166666 | 0.0220947265625 | 0.0114013671875 | 0.00911458333333333 | 0.268899356617646 |
2048 | 0.0161946614583333 | 0.01998291015625 | 0.0214925130208333 | 0.0114828853283898 | 0.00921527409957627 | 0.278952205882353 |
4096 | 0.0144460867117117 | 0.0185825280975877 | 0.0206277412280702 | 0.0108415721792035 | 0.0084239318307522 | 0.267947520380435 |
8192 | 0.0142745284346846 | 0.0182640953401548 | 0.0203025615320796 | 0.0106473650251116 | 0.00824909382038287 | 0.262157985142299 |
16384 | 0.0141559103260869 | 0.0179805249239491 | 0.020755573854608 | 0.0107565487132353 | 0.00812639508928571 | 0.250493730817522 |
32768 | 0.0140704774020011 | 0.0179896923380161 | 0.0200802914731137 | 0.0106380529570998 | 0.00813737349076703 | 0.258771379417348 |
65536 | 0.0146707388070913 | 0.0183997835431779 | 0.0204668546977795 | 0.0105399741782798 | 0.00816053910688919 | 0.268095340368881 |
131072 | 0.0146493230547224 | 0.0185520989554269 | 0.0207954738451086 | 0.0107275310315584 | 0.0088447122012867 | 0.258940424237932 |
262144 | 0.0140965325491769 | 0.0189025038379734 | 0.0208538647355704 | 0.0108903583727385 | 0.0081349666987624 | 0.263193497964001 |
524288 | 0.0142521899679433 | 0.0186717099156873 | 0.0204430128398693 | 0.0108228921890259 | 0.00825509079941757 | 0.266959584087406 |
1048576 | 0.014292804473037 | 0.0184026333170199 | 0.020549007824489 | 0.0106714248657227 | 0.00817386593137468 | 0.267729050290268 |
2097152 | 0.0145983433504717 | 0.0193452234027766 | 0.0205662584750452 | 0.0109894586646039 | 0.00826820321039319 | 0.267035618695345 |
4194304 | 0.014525378515961 | 0.0186883622566155 | 0.020822372701433 | 0.0107787522402677 | 0.00840708613395691 | 0.265018746256828 |
8388608 | 0.0150390152345623 | 0.0188832973179064 | 0.0209382815020424 | 0.0109388123387876 | 0.00840827822685242 | 0.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.