Conditional Foreach Performance Considerations in DotNet (C#)

Posted in software by Christopher R. Wirz on Sat Jun 03 2017

When operating on large data sets in .Net, there is frequently a need to cancel the operations. We have already determined the steady-state performance considerations of basic loop structures, but adding a cancellation check may change that. As determined before, Arrays are generally the faster structure except when using .Net's MoveNext(). for loops are the fastest, followed closely by foreach.

This quick study hopes to see if there is any change in which to select based on the use of a cancellation token.
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);
	var token = default(CancellationToken);

	// for loop on List
	var current = 0;
	sw.Start();
	for (int i = 0; !token.IsCancellationRequested && 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)
	{
		if (token.IsCancellationRequested) break;
		current += val;
	}
	sw.Stop();
	ret[1] = sw.Elapsed.Ticks;

	// enumerator MoveNext on List
	current = 0;
	sw.Restart();
	var enumerator = testList.GetEnumerator();
	while (!token.IsCancellationRequested && 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; !token.IsCancellationRequested && 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)
	{
		if (token.IsCancellationRequested) break;
		current += val;
	}
	sw.Stop();
	ret[4] = sw.Elapsed.Ticks;

	// enumerator MoveNext on Array
	current = 0;
	sw.Restart();
	var aenumerator = testArray.GetEnumerator();
	while (!token.IsCancellationRequested && 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.033613445378153.159663865546211.666666666666661.61.366666666666668.45378151260504
22.233333333333333.851.941666666666661.966666666666661.733333333333337.06666666666666
42.008333333333333.991666666666662.416666666666662.21.433333333333338.025
82.833333333333334.591666666666662.576271186440672.341666666666661.8333333333333313.2083333333333
163.755.741666666666663.466666666666662.566666666666662.6333333333333321.8547008547008
325.816666666666668.566666666666667.2254.741666666666664.8333333333333351.798319327731
649.5416666666666612.57510.73333333333336.983333333333337.2833333333333373.054054054054
12813.808333333333319.891666666666618.916666666666610.591666666666611.3333333333333131.552631578947
25625.182608695652136.633333333333337.133333333333321.322.5267.306306306306
51249.89565217391365.09909909909968.627118644067738.206896551724142.0086206896551586.798319327731
102497.6521739130434130.149122807017135.70175438596475.582.96521739130431082.64912280701
2048196.310344827586257.173913043478271.163793103448150.700854700854166.8717948717942137.36936936936
4096392.625507.410714285714540.9375296.6334.0603448275864226.71028037383
8192792.3043478260871023.663716814151087.60526315789602.438596491228661.8596491228078677.02970297029
163841533.76106194692037.643478260862184.008695652171194.56251327.3451327433618045.4594594594
327683134.689655172414098.405172413794372.201754385962411.180180180182658.2212389380535156.2432432432
655366259.086206896558147.834782608698693.796460176994785.672727272725292.7818181818169850.7663551402
13107212254.438596491216153.454545454517381.89782.6306306306310798.0982142857142121.089285714
26214425424.690265486733068.666666666635065.908256880719155.166666666621315.0091743119288966.622807017
52428850529.098214285765797.557522123970636.619469026539647.068376068343569.3063063063577622.45045045
1048576101330.371681415131997.855855855140286.25471698177389.300884955785306.72072072071148081.91452991
2097152204322.308411214267888.166666666281648.897196261155159.216216216173218.1160714282301453.94642857


Now, normalize the times to ticks per item.



Normalized Average Time (with respect to ticks / item)

itemsfor (List)foreach (List)enumerator (List)for (Array)foreach (Array)enumerator (Array)
40.5084033613445370.7899159663865520.4166666666666650.40.3416666666666652.11344537815126
80.2791666666666660.481250.2427083333333320.2458333333333320.2166666666666660.883333333333333
160.1255208333333330.2494791666666660.1510416666666660.13750.08958333333333310.5015625
320.08854166666666660.1434895833333330.08050847457627090.07317708333333310.05729166666666660.412760416666666
640.058593750.08971354166666660.05416666666666660.04010416666666660.04114583333333330.3414797008547
1280.04544270833333330.06692708333333330.05644531250.03704427083333330.03776041666666660.404674369747898
2560.03727213541666660.049121093750.04192708333333320.02727864583333330.02845052083333330.285367398648648
5120.02696940104166660.03885091145833320.03694661458333320.02068684895833320.02213541666666660.256938733552631
10240.02459239130434780.03577473958333330.03626302083333330.020800781250.021972656250.261041314752252
20480.02436311141304350.03178666948198190.03350933527542370.01865571120689650.02051202182112070.286522616859244
40960.02384086277173910.03177468818530690.03313031112938570.01843261718750.02025518002717390.264318633497805
81920.02396366514008620.03139329993206520.03310104896282320.01839610042735030.02037009214743580.260909346846846
163840.02396392822265620.03096989222935270.03301620483398440.018103027343750.02038942534348060.257977922386098
327680.02417920983355980.03123973745160370.03319107858758210.01838496693393640.02019835354989040.264801931853341
655360.02340333651652370.03109197201936130.03332532799762220.01822757720947270.0202536793936670.275351859427786
1310720.02391578411233830.03126835000926050.03335725215443390.01839584488052510.02028061858320660.268220849939294
2621440.02387651903875940.03108152306598160.03316420158453750.01825589266690340.02019036032936790.266459527416764
5242880.02337348670290220.03081026944247150.03315315246582030.0186588871586430.02059573786599290.27107446534293
10485760.02424687410877870.03153673807779940.0334414560860450.01826779047648110.02032757680350480.275580046469705
20971520.02409415159906660.03137472034555620.03368216489268610.01890519541552940.02077546420398060.275431847787118
41943040.02415904323611620.03147074123760580.03344684951710250.01845104715465440.0203387071420480.273724058754423
83886080.02435711722507640.03193475802739450.03357516493752730.01849641993239120.02064920855419970.274354689887592


Show the results on a log2 - log2 plot...





At first, we see that adding the cancellation check has also added ~20 nanoseconds (there are 100 nanoseconds / tick). As expected, for and foreach loops on Arrays are fastest. Using the enumerator's MoveNext() on the List is slower than calling foreach, but not by much - and this gap is narrower then when not checking for a cancellation. The enumerator with MoveNext() as it is always slower.

The Array enumerator is still the slowest.

In conclusion, adding a cancellation check does not change the selection of a loop type. It is still recommended to use for loops, then foreach loops, then enumerators MoveNext().