In .Net, Which Loop Runs Faster, 'For' or 'Foreach'

In .NET, which loop runs faster, 'for' or 'foreach'?

Patrick Smacchia blogged about this last month, with the following conclusions:

  • for loops on List are a bit more than 2 times cheaper than foreach
    loops on List.
  • Looping on array is around 2 times cheaper than looping on List.
  • As a consequence, looping on array using for is 5 times cheaper
    than looping on List using foreach
    (which I believe, is what we all do).

foreach 20 times slower than for?

A foreach loop requires the use of an Enumerator to iterate over the collection, which requires accessing the Current property and calling the MoveNext method on each iteration, which take some processing time.

The for loop only has to call get_Item on each iteration, so that’s one less call than the foreach loop, which makes a slight difference in performance.

Why is my foreach faster than my for loop?

The reason is explained in detailed in this article. it says that

In micro-benchmarking, introducing extra local variables with
foreach-loops can impact performance. However, if those local
variables are reused several times in the loop body, they can lead to
a performance improvement.

Thus: The for-loop is faster than the foreach-loop if the array must
only be accessed once per iteration.

You can realize this by including some operations in the loop and runs it again.

Looping for vs foreach difference in iterating dictionary c#

The lack of any kind of indexer in the IDictionary<> interface due to dictionaries not having a defined ordering can make it difficult to iterate without the use of foreach/GetEnumerator(). Given...

Dictionary<int, int> dictionary = Enumerable.Range(0, 10).ToDictionary(i => i, i => -i);

...since you know the keys comprise a contiguous range of integers, you can use for to loop through all possible key values...

// This exploits the fact that we know keys from 0..9 exist in dictionary
for (int key = 0; key < dictionary.Count; key++)
{
int value = dictionary[key];

// ...
}

If you can't make that assumption, however, it becomes much trickier. You could iterate the Keys collection property to get each element's key...but that collection doesn't allow indexing, either, so you're back where you started with the foreach vs. for dilemma. If you insist on using for, though, one way to do so is by copying Keys to an array and then iterating that...

// Copy the Keys property to an array to allow indexing
int[] keys = new int[dictionary.Count];
dictionary.Keys.CopyTo(keys, 0);

// This makes no assumptions about the distribution of keys in dictionary
for (int index = 0; index < dictionary.Count; index++)
{
int key = keys[index];
int value = Source[key];

// ...
}

Of course, CopyTo() will enumerate Keys one complete time before you even have a chance to do so yourself, so that can only hurt performance.

If you are working with a fixed set of keys that's known ahead of time, or you don't mind having to maintain a separate collections of keys every time the dictionary's keys change, a slightly better way is to cache the keys in a structure that can be indexed...

int[] keyCache = Enumerable.Range(0, 10).ToArray();

// ...

// This retrieves known keys stored separately from dictionary
for (int index = 0; index < keyCache.Length; index++)
{
int key = keyCache[index];
int value = dictionary[key];

// ...
}

It might be tempting to use the LINQ ElementAt() method instead; after all, it's easy enough to use...

for (int index = 0; index < dictionary.Count; index++)
{
KeyValuePair<int, int> pair = dictionary.ElementAt(index);

// ...
}

This is very bad for performance, however. ElementAt() can only special-case for indexing when the input collection implements IList<>, which Dictionary<> does not nor does IDictionary<> inherit from it. Otherwise, for every index you try to retrieve it has to start from the beginning. Consider enumerating the entire 10-element dictionary defined above...


| Index requested | Elements enumerated | Total elements enumerated |
|:---------------:|:----------------------------:|:-------------------------:|
| 0 | 0 | 1 |
| 1 | 0, 1 | 3 |
| 2 | 0, 1, 2 | 6 |
| 3 | 0, 1, 2, 3 | 10 |
| 4 | 0, 1, 2, 3, 4 | 15 |
| 5 | 0, 1, 2, 3, 4, 5 | 21 |
| 6 | 0, 1, 2, 3, 4, 5, 6 | 28 |
| 7 | 0, 1, 2, 3, 4, 5, 6, 7 | 36 |
| 8 | 0, 1, 2, 3, 4, 5, 6, 7, 8 | 45 |
| 9 | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 | 55 |

Add all that up and it will take 55 enumerations to step through a 10-element dictionary! So, in an effort to improve performance by eliminating foreach/GetEnumerator() this has only moved the GetEnumerator() call under the covers and made performance worse.

As for the actual performance differences of these approaches, here are the results I got...


// * Summary *

BenchmarkDotNet=v0.12.1, OS=Windows 10.0.18363.657 (1909/November2018Update/19H2)
Intel Core i7 CPU 860 2.80GHz (Nehalem), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=3.1.201
[Host] : .NET Core 3.1.3 (CoreCLR 4.700.20.11803, CoreFX 4.700.20.12001), X64 RyuJIT
.NET 4.8 : .NET Framework 4.8 (4.8.4121.0), X64 RyuJIT
.NET Core 3.1 : .NET Core 3.1.3 (CoreCLR 4.700.20.11803, CoreFX 4.700.20.12001), X64 RyuJIT


| Method | Job | Runtime | Size | Mean | Error | StdDev | Ratio | RatioSD |
|---------------------------------- |-------------- |-------------- |------- |--------------------:|------------------:|------------------:|----------:|--------:|
| GetEnumerator | .NET 4.8 | .NET 4.8 | 10 | 118.4 ns | 1.71 ns | 1.76 ns | 1.02 | 0.02 |
| ForEach | .NET 4.8 | .NET 4.8 | 10 | 116.4 ns | 1.44 ns | 1.28 ns | 1.00 | 0.00 |
| For_Indexer_ConsecutiveKeys | .NET 4.8 | .NET 4.8 | 10 | 147.6 ns | 2.96 ns | 3.17 ns | 1.26 | 0.02 |
| While_Indexer_ConsecutiveKeys | .NET 4.8 | .NET 4.8 | 10 | 149.2 ns | 1.72 ns | 1.61 ns | 1.28 | 0.02 |
| For_TryGetValue_ConsecutiveKeys | .NET 4.8 | .NET 4.8 | 10 | 154.5 ns | 1.16 ns | 0.97 ns | 1.33 | 0.01 |
| While_TryGetValue_ConsecutiveKeys | .NET 4.8 | .NET 4.8 | 10 | 160.8 ns | 1.93 ns | 1.71 ns | 1.38 | 0.01 |
| For_Indexer_CopyToKeys | .NET 4.8 | .NET 4.8 | 10 | 177.5 ns | 1.37 ns | 1.14 ns | 1.53 | 0.02 |
| While_Indexer_CopyToKeys | .NET 4.8 | .NET 4.8 | 10 | 185.6 ns | 3.69 ns | 4.80 ns | 1.59 | 0.05 |
| For_Indexer_CachedKeys | .NET 4.8 | .NET 4.8 | 10 | 154.5 ns | 2.83 ns | 2.64 ns | 1.33 | 0.03 |
| While_Indexer_CachedKeys | .NET 4.8 | .NET 4.8 | 10 | 155.3 ns | 2.35 ns | 2.08 ns | 1.33 | 0.02 |
| For_ElementAt | .NET 4.8 | .NET 4.8 | 10 | 1,009.2 ns | 8.61 ns | 7.19 ns | 8.67 | 0.12 |
| While_ElementAt | .NET 4.8 | .NET 4.8 | 10 | 1,140.9 ns | 14.36 ns | 13.43 ns | 9.81 | 0.16 |
| | | | | | | | | |
| GetEnumerator | .NET Core 3.1 | .NET Core 3.1 | 10 | 118.6 ns | 2.39 ns | 3.19 ns | 0.98 | 0.03 |
| ForEach | .NET Core 3.1 | .NET Core 3.1 | 10 | 120.3 ns | 1.28 ns | 1.14 ns | 1.00 | 0.00 |
| For_Indexer_ConsecutiveKeys | .NET Core 3.1 | .NET Core 3.1 | 10 | 126.1 ns | 0.67 ns | 0.56 ns | 1.05 | 0.01 |
| While_Indexer_ConsecutiveKeys | .NET Core 3.1 | .NET Core 3.1 | 10 | 135.5 ns | 2.28 ns | 2.02 ns | 1.13 | 0.02 |
| For_TryGetValue_ConsecutiveKeys | .NET Core 3.1 | .NET Core 3.1 | 10 | 131.0 ns | 2.41 ns | 2.25 ns | 1.09 | 0.02 |
| While_TryGetValue_ConsecutiveKeys | .NET Core 3.1 | .NET Core 3.1 | 10 | 133.9 ns | 1.42 ns | 1.19 ns | 1.11 | 0.01 |
| For_Indexer_CopyToKeys | .NET Core 3.1 | .NET Core 3.1 | 10 | 162.4 ns | 2.32 ns | 2.06 ns | 1.35 | 0.02 |
| While_Indexer_CopyToKeys | .NET Core 3.1 | .NET Core 3.1 | 10 | 166.3 ns | 1.29 ns | 1.21 ns | 1.38 | 0.02 |
| For_Indexer_CachedKeys | .NET Core 3.1 | .NET Core 3.1 | 10 | 136.0 ns | 1.27 ns | 1.19 ns | 1.13 | 0.02 |
| While_Indexer_CachedKeys | .NET Core 3.1 | .NET Core 3.1 | 10 | 142.3 ns | 2.84 ns | 4.59 ns | 1.14 | 0.02 |
| For_ElementAt | .NET Core 3.1 | .NET Core 3.1 | 10 | 952.4 ns | 10.08 ns | 8.94 ns | 7.92 | 0.13 |
| While_ElementAt | .NET Core 3.1 | .NET Core 3.1 | 10 | 953.8 ns | 8.86 ns | 7.40 ns | 7.93 | 0.12 |
| | | | | | | | | |
| GetEnumerator | .NET 4.8 | .NET 4.8 | 1000 | 9,344.9 ns | 80.50 ns | 75.30 ns | 1.00 | 0.01 |
| ForEach | .NET 4.8 | .NET 4.8 | 1000 | 9,360.2 ns | 82.04 ns | 64.05 ns | 1.00 | 0.00 |
| For_Indexer_ConsecutiveKeys | .NET 4.8 | .NET 4.8 | 1000 | 15,122.4 ns | 81.71 ns | 68.23 ns | 1.62 | 0.01 |
| While_Indexer_ConsecutiveKeys | .NET 4.8 | .NET 4.8 | 1000 | 15,106.4 ns | 85.68 ns | 75.96 ns | 1.61 | 0.02 |
| For_TryGetValue_ConsecutiveKeys | .NET 4.8 | .NET 4.8 | 1000 | 16,160.3 ns | 270.09 ns | 252.64 ns | 1.73 | 0.03 |
| While_TryGetValue_ConsecutiveKeys | .NET 4.8 | .NET 4.8 | 1000 | 16,452.4 ns | 146.51 ns | 129.88 ns | 1.76 | 0.02 |
| For_Indexer_CopyToKeys | .NET 4.8 | .NET 4.8 | 1000 | 17,407.1 ns | 251.38 ns | 222.84 ns | 1.86 | 0.03 |
| While_Indexer_CopyToKeys | .NET 4.8 | .NET 4.8 | 1000 | 17,034.0 ns | 295.71 ns | 404.77 ns | 1.85 | 0.05 |
| For_Indexer_CachedKeys | .NET 4.8 | .NET 4.8 | 1000 | 16,277.5 ns | 69.91 ns | 58.38 ns | 1.74 | 0.02 |
| While_Indexer_CachedKeys | .NET 4.8 | .NET 4.8 | 1000 | 15,131.9 ns | 55.97 ns | 46.74 ns | 1.62 | 0.01 |
| For_ElementAt | .NET 4.8 | .NET 4.8 | 1000 | 4,859,257.3 ns | 18,862.72 ns | 15,751.22 ns | 519.24 | 4.36 |
| While_ElementAt | .NET 4.8 | .NET 4.8 | 1000 | 3,837,001.5 ns | 7,396.43 ns | 6,556.74 ns | 409.85 | 3.11 |
| | | | | | | | | |
| GetEnumerator | .NET Core 3.1 | .NET Core 3.1 | 1000 | 9,029.9 ns | 21.69 ns | 18.12 ns | 1.00 | 0.00 |
| ForEach | .NET Core 3.1 | .NET Core 3.1 | 1000 | 9,022.4 ns | 13.08 ns | 10.92 ns | 1.00 | 0.00 |
| For_Indexer_ConsecutiveKeys | .NET Core 3.1 | .NET Core 3.1 | 1000 | 11,396.9 ns | 18.42 ns | 15.38 ns | 1.26 | 0.00 |
| While_Indexer_ConsecutiveKeys | .NET Core 3.1 | .NET Core 3.1 | 1000 | 12,504.6 ns | 13.82 ns | 10.79 ns | 1.39 | 0.00 |
| For_TryGetValue_ConsecutiveKeys | .NET Core 3.1 | .NET Core 3.1 | 1000 | 12,244.1 ns | 73.90 ns | 69.13 ns | 1.36 | 0.01 |
| While_TryGetValue_ConsecutiveKeys | .NET Core 3.1 | .NET Core 3.1 | 1000 | 12,437.4 ns | 22.48 ns | 18.77 ns | 1.38 | 0.00 |
| For_Indexer_CopyToKeys | .NET Core 3.1 | .NET Core 3.1 | 1000 | 13,717.9 ns | 36.98 ns | 30.88 ns | 1.52 | 0.00 |
| While_Indexer_CopyToKeys | .NET Core 3.1 | .NET Core 3.1 | 1000 | 14,099.6 ns | 20.44 ns | 18.12 ns | 1.56 | 0.00 |
| For_Indexer_CachedKeys | .NET Core 3.1 | .NET Core 3.1 | 1000 | 12,640.4 ns | 23.31 ns | 19.47 ns | 1.40 | 0.00 |
| While_Indexer_CachedKeys | .NET Core 3.1 | .NET Core 3.1 | 1000 | 12,610.5 ns | 20.97 ns | 17.51 ns | 1.40 | 0.00 |
| For_ElementAt | .NET Core 3.1 | .NET Core 3.1 | 1000 | 3,402,799.3 ns | 15,880.59 ns | 14,077.73 ns | 377.13 | 1.73 |
| While_ElementAt | .NET Core 3.1 | .NET Core 3.1 | 1000 | 3,399,305.2 ns | 5,822.01 ns | 5,161.06 ns | 376.76 | 0.74 |
| | | | | | | | | |
| GetEnumerator | .NET 4.8 | .NET 4.8 | 100000 | 885,621.4 ns | 1,617.29 ns | 1,350.51 ns | 1.00 | 0.00 |
| ForEach | .NET 4.8 | .NET 4.8 | 100000 | 884,591.8 ns | 1,781.29 ns | 1,390.72 ns | 1.00 | 0.00 |
| For_Indexer_ConsecutiveKeys | .NET 4.8 | .NET 4.8 | 100000 | 1,424,062.0 ns | 2,791.28 ns | 2,474.39 ns | 1.61 | 0.00 |
| While_Indexer_ConsecutiveKeys | .NET 4.8 | .NET 4.8 | 100000 | 1,435,667.1 ns | 3,696.89 ns | 3,277.19 ns | 1.62 | 0.00 |
| For_TryGetValue_ConsecutiveKeys | .NET 4.8 | .NET 4.8 | 100000 | 1,502,486.1 ns | 3,750.98 ns | 3,325.15 ns | 1.70 | 0.00 |
| While_TryGetValue_ConsecutiveKeys | .NET 4.8 | .NET 4.8 | 100000 | 1,558,335.7 ns | 4,619.63 ns | 3,857.60 ns | 1.76 | 0.00 |
| For_Indexer_CopyToKeys | .NET 4.8 | .NET 4.8 | 100000 | 1,685,000.7 ns | 4,676.88 ns | 3,651.40 ns | 1.90 | 0.01 |
| While_Indexer_CopyToKeys | .NET 4.8 | .NET 4.8 | 100000 | 1,722,418.0 ns | 3,431.67 ns | 3,042.08 ns | 1.95 | 0.01 |
| For_Indexer_CachedKeys | .NET 4.8 | .NET 4.8 | 100000 | 1,499,782.0 ns | 2,951.84 ns | 2,616.73 ns | 1.70 | 0.00 |
| While_Indexer_CachedKeys | .NET 4.8 | .NET 4.8 | 100000 | 1,583,570.2 ns | 3,880.57 ns | 3,440.03 ns | 1.79 | 0.00 |
| For_ElementAt | .NET 4.8 | .NET 4.8 | 100000 | 37,917,621,633.3 ns | 47,744,618.60 ns | 44,660,345.86 ns | 42,868.63 | 93.80 |
| While_ElementAt | .NET 4.8 | .NET 4.8 | 100000 | 38,343,003,642.9 ns | 262,502,616.47 ns | 232,701,732.10 ns | 43,315.66 | 229.53 |
| | | | | | | | | |
| GetEnumerator | .NET Core 3.1 | .NET Core 3.1 | 100000 | 900,980.9 ns | 2,477.29 ns | 2,068.65 ns | 1.00 | 0.00 |
| ForEach | .NET Core 3.1 | .NET Core 3.1 | 100000 | 899,775.7 ns | 1,040.30 ns | 868.70 ns | 1.00 | 0.00 |
| For_Indexer_ConsecutiveKeys | .NET Core 3.1 | .NET Core 3.1 | 100000 | 1,177,153.8 ns | 1,689.80 ns | 1,411.06 ns | 1.31 | 0.00 |
| While_Indexer_ConsecutiveKeys | .NET Core 3.1 | .NET Core 3.1 | 100000 | 1,255,795.4 ns | 2,562.23 ns | 2,139.58 ns | 1.40 | 0.00 |
| For_TryGetValue_ConsecutiveKeys | .NET Core 3.1 | .NET Core 3.1 | 100000 | 1,226,163.3 ns | 2,317.36 ns | 1,809.25 ns | 1.36 | 0.00 |
| While_TryGetValue_ConsecutiveKeys | .NET Core 3.1 | .NET Core 3.1 | 100000 | 1,245,130.0 ns | 4,146.38 ns | 3,237.22 ns | 1.38 | 0.00 |
| For_Indexer_CopyToKeys | .NET Core 3.1 | .NET Core 3.1 | 100000 | 1,430,340.4 ns | 7,834.82 ns | 6,945.37 ns | 1.59 | 0.01 |
| While_Indexer_CopyToKeys | .NET Core 3.1 | .NET Core 3.1 | 100000 | 1,472,807.7 ns | 5,363.80 ns | 4,754.87 ns | 1.64 | 0.01 |
| For_Indexer_CachedKeys | .NET Core 3.1 | .NET Core 3.1 | 100000 | 1,289,902.4 ns | 2,739.78 ns | 2,139.04 ns | 1.43 | 0.00 |
| While_Indexer_CachedKeys | .NET Core 3.1 | .NET Core 3.1 | 100000 | 1,276,484.8 ns | 4,652.23 ns | 3,884.82 ns | 1.42 | 0.00 |
| For_ElementAt | .NET Core 3.1 | .NET Core 3.1 | 100000 | 33,717,209,257.1 ns | 200,565,125.50 ns | 177,795,759.65 ns | 37,460.45 | 216.07 |
| While_ElementAt | .NET Core 3.1 | .NET Core 3.1 | 100000 | 34,064,932,086.7 ns | 225,399,893.36 ns | 210,839,200.10 ns | 37,841.10 | 204.02 |

...from this little program I wrote using BenchmarkDotNet...

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;

namespace SO61507883
{
[SimpleJob(RuntimeMoniker.Net48)]
[SimpleJob(RuntimeMoniker.NetCoreApp31)]
public class Benchmarks
{
public static IReadOnlyList<int> DictionarySizes
{
get;
} = Array.AsReadOnly(new int[] { 10, 1_000 });

[ParamsSource(nameof(DictionarySizes))]
public int Size
{
get; set;
}

public Dictionary<int, int> Source
{
get; set;
}

// Only used by the *_CachedKeys() benchmark methods
public int[] KeyCache
{
get; set;
}

[GlobalSetup()]
public void Setup()
{
Source = Enumerable.Range(0, Size)
.ToDictionary(i => i, i => -i);

KeyCache = new int[Size];
Source.Keys.CopyTo(KeyCache, 0);
}

[Benchmark()]
public (int keySum, int valueSum) GetEnumerator()
{
int keySum = 0;
int valueSum = 0;

using (Dictionary<int, int>.Enumerator enumerator = Source.GetEnumerator())
while (enumerator.MoveNext())
{
KeyValuePair<int, int> pair = enumerator.Current;

keySum += pair.Key;
valueSum += pair.Value;
}

return (keySum, valueSum);
}

[Benchmark(Baseline = true)]
public (int keySum, int valueSum) ForEach()
{
int keySum = 0;
int valueSum = 0;

foreach (KeyValuePair<int, int> pair in Source)
{
keySum += pair.Key;
valueSum += pair.Value;
}

return (keySum, valueSum);
}

[Benchmark()]
public (int keySum, int valueSum) For_Indexer_ConsecutiveKeys()
{
int keySum = 0;
int valueSum = 0;

// This exploits the fact that we know keys from 0..Size-1 exist in Source
for (int key = 0; key < Size; key++)
{
int value = Source[key];

keySum += key;
valueSum += value;
}

return (keySum, valueSum);
}

[Benchmark()]
public (int keySum, int valueSum) While_Indexer_ConsecutiveKeys()
{
int key = 0;
int keySum = 0;
int valueSum = 0;

// This exploits the fact that we know keys from 0..Size-1 exist in Source
while (key < Size)
{
int value = Source[key];

keySum += key++;
valueSum += value;
}

return (keySum, valueSum);
}

[Benchmark()]
public (int keySum, int valueSum) For_TryGetValue_ConsecutiveKeys()
{
int keySum = 0;
int valueSum = 0;

// This exploits the fact that we know keys from 0..Size-1 exist in Source
for (int key = 0; key < Size; key++)
if (Source.TryGetValue(key, out int value))
{
keySum += key;
valueSum += value;
}

return (keySum, valueSum);
}

[Benchmark()]
public (int keySum, int valueSum) While_TryGetValue_ConsecutiveKeys()
{
int key = 0;
int keySum = 0;
int valueSum = 0;

// This exploits the fact that we know keys from 0..Size-1 exist in Source
while (key < Size)
if (Source.TryGetValue(key, out int value))
{
keySum += key++;
valueSum += value;
}

return (keySum, valueSum);
}

[Benchmark()]
public (int keySum, int valueSum) For_Indexer_CopyToKeys()
{
// Copy the Keys property to an array to allow indexing
int[] keys = new int[Size];
Source.Keys.CopyTo(keys, 0);

int keySum = 0;
int valueSum = 0;

// This makes no assumptions about the distribution of keys in Source
for (int index = 0; index < Size; index++)
{
int key = keys[index];
int value = Source[key];

keySum += key;
valueSum += value;
}

return (keySum, valueSum);
}

[Benchmark()]
public (int keySum, int valueSum) While_Indexer_CopyToKeys()
{
// Copy the Keys property to an array to allow indexing
int[] keys = new int[Size];
Source.Keys.CopyTo(keys, 0);

int index = 0;
int keySum = 0;
int valueSum = 0;

// This makes no assumptions about the distribution of keys in Source
while (index < Size)
{
int key = keys[index++];
int value = Source[key];

keySum += key;
valueSum += value;
}

return (keySum, valueSum);
}

[Benchmark()]
public (int keySum, int valueSum) For_Indexer_CachedKeys()
{
int keySum = 0;
int valueSum = 0;

// This retrieves known keys stored separately from Source
for (int index = 0; index < Size; index++)
{
int key = KeyCache[index];
int value = Source[key];

keySum += key;
valueSum += value;
}

return (keySum, valueSum);
}

[Benchmark()]
public (int keySum, int valueSum) While_Indexer_CachedKeys()
{
int index = 0;
int keySum = 0;
int valueSum = 0;

// This retrieves known keys stored separately from Source
while (index < Size)
{
int key = KeyCache[index++];
int value = Source[key];

keySum += key;
valueSum += value;
}

return (keySum, valueSum);
}

[Benchmark()]
public (int keySum, int valueSum) For_ElementAt()
{
int keySum = 0;
int valueSum = 0;

for (int index = 0; index < Size; index++)
{
KeyValuePair<int, int> pair = Source.ElementAt(index);

keySum += pair.Key;
valueSum += pair.Value;
}

return (keySum, valueSum);
}

[Benchmark()]
public (int keySum, int valueSum) While_ElementAt()
{
int index = 0;
int keySum = 0;
int valueSum = 0;

while (index < Size)
{
KeyValuePair<int, int> pair = Source.ElementAt(index++);

keySum += pair.Key;
valueSum += pair.Value;
}

return (keySum, valueSum);
}
}

static class Program
{
static void Main(string[] args)
{
switch (args?.FirstOrDefault()?.ToUpper())
{
case "BENCHMARK":
BenchmarkMethods();
break;
case "TEST":
TestMethods();
break;
default:
DisplayUsage();
break;
}
}

static void DisplayUsage()
{
string assemblyLocation = Assembly.GetEntryAssembly().Location;
string assemblyFileName = System.IO.Path.GetFileName(assemblyLocation);

Console.WriteLine($"{assemblyFileName} {{ BENCHMARK | TEST }}");
Console.WriteLine("\tBENCHMARK - Benchmark dictionary enumeration methods.");
Console.WriteLine("\t TEST - Display results of dictionary enumeration methods.");
}

static void BenchmarkMethods()
{
BenchmarkDotNet.Running.BenchmarkRunner.Run<Benchmarks>();
}

static void TestMethods()
{
// Find, setup, and call the benchmark methods the same way BenchmarkDotNet would
Benchmarks benchmarks = new Benchmarks();
IEnumerable<MethodInfo> benchmarkMethods = benchmarks.GetType()
.GetMethods()
.Where(
method => method.CustomAttributes.Any(
attributeData => typeof(BenchmarkAttribute).IsAssignableFrom(attributeData.AttributeType)
)
);

foreach (MethodInfo method in benchmarkMethods)
{
Console.WriteLine($"{method.Name}():");
foreach (int size in Benchmarks.DictionarySizes)
{
benchmarks.Size = size;
benchmarks.Setup();
(int, int) result = ((int, int)) method.Invoke(benchmarks, Array.Empty<object>());

Console.WriteLine($"\t{size:N0} elements => {result}");
}
}
}
}
}

Note that the code above omits 100_000 from the Benchmarks.DictionarySizes property because it adds more than an hour to the run time.

Conclusions:

  • foreach/GetEnumerator() are the fastest ways to iterate a dictionary.
  • Depending on the runtime it's, at best, slightly slower using a for or while loop when you can make some assumptions about your keys, but it's still slower.
  • Using ElementAt() inside a for loop has terrible performance that only gets slower the bigger the dictionary gets.

In .NET, which loop runs faster, 'for' or 'foreach'?

Patrick Smacchia blogged about this last month, with the following conclusions:

  • for loops on List are a bit more than 2 times cheaper than foreach
    loops on List.
  • Looping on array is around 2 times cheaper than looping on List.
  • As a consequence, looping on array using for is 5 times cheaper
    than looping on List using foreach
    (which I believe, is what we all do).

Performance difference for control structures 'for' and 'foreach' in C#

Well, it partly depends on the exact type of list. It will also depend on the exact CLR you're using.

Whether it's in any way significant or not will depend on whether you're doing any real work in the loop. In almost all cases, the difference to performance won't be significant, but the difference to readability favours the foreach loop.

I'd personally use LINQ to avoid the "if" too:

foreach (var item in list.Where(condition))
{
}


Related Topics



Leave a reply



Submit