0

I have a below foreach loop which does the job. I am curious to know for my below case - is it better to use for loop instead of foreach loop for performance issues?

Since I read it that for loop is faster than foreach loop so I am kinda confuse as well.

  foreach (KeyValuePair<string, StringValues> v in values)
  {
      string key = v.Key;
      StringValues val = v.Value;
      if (val.Count > 0)
      {
          if (!string.IsNullOrWhiteSpace(val[0]))
          {
              switch (key)
              {
                  case ABC:
                      One = val[0];
                      break;
                  case PQR:
                      Two = val[0];
                      break;
                  //.. bunch of other case block here with similar stuff
              }
          }
      }
  }
Lance U. Matthews
  • 15,725
  • 6
  • 48
  • 68
dragons
  • 549
  • 1
  • 8
  • 24
  • 3
    Since you're asking about this specific case and you're the one who already has this code running, you could always...just try it. We can't really answer, anyways, because we don't know the specific type of `values` (presumably something that allows indexing). Otherwise, have you identified a specific performance issue with enumerating `values`? Is slightly longer, slightly less readable code a worthwhile trade-off to address that issue? – Lance U. Matthews Apr 29 '20 at 17:38
  • 3
    It's a micro-optimization. Don't worry about it. IMO `foreach` loops are *slightly* easier to read. That's the important aspect at the moment. The intent is (usually) more clear and it'll be easier to maintain. – Broots Waymb Apr 29 '20 at 17:38
  • 1
    Easy - build test case and measure performance. Like your linked article (and its comments) shows, it depends :) – Arvo Apr 29 '20 at 17:39
  • It's hard to tell. According to resource you provided, it is better to use `foreach` when accessing an element multiple times. You access `kvp` twice and then use it's `Value`. You access `Value` directly through `val` variable, so it doesn't count as another access to the `kvp`. But it is the question for these two accesses is it better to use `for`, or `foreach` loop. – Marko Radivojević Apr 29 '20 at 17:43
  • yeah that's what I am confuse and it looks like there might be little difference only but trying to understand what will take most time in foreach vs for loop for my case. – dragons Apr 29 '20 at 18:03
  • 1
    [This answer](https://stackoverflow.com/a/59053661/150605) to [What is the best way to iterate over a dictionary?](https://stackoverflow.com/q/141088/150605) provides some (seemingly flawed) performance comparisons. The code in [this answer](https://stackoverflow.com/a/5265690/150605) on the same question might [look familiar](https://stackoverflow.com/a/61508503/150605). Pay no attention to the overwhelmingly positive score; the real story is in the comments. – Lance U. Matthews Apr 29 '20 at 18:42
  • yeah you talking about spender comments right? I saw that earlier and thats where I got confuse with Filip answer of using elementAt. So if I have to use for loop to iterate dictionary - how should I do it then efficiently? – dragons Apr 29 '20 at 18:44
  • 2
    Wow, I'm glad I got my answer in before this got marked a duplicate (knew that would happen), but **I don't think this is a duplicate**. The linked question and many of its answers seem to be focusing on arrays, `IList`, and types that otherwise provide random-access indexing, whereas this question is about dictionaries, which none of that question's answers even mention. So, this is either a _more specific_ question or a different question altogether, but, either way, not a duplicate, in my opinion. The question I linked doesn't really work, either, because "best way" is undefined. – Lance U. Matthews Apr 29 '20 at 21:35

2 Answers2

4

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.
Lance U. Matthews
  • 15,725
  • 6
  • 48
  • 68
  • How did you run your file? I got `benchmarkdotnet` installed and when I run like this `dotnet run -p Benchmarker/Benchmarker.csproj -c Release` by using your program in my `Progam.cs` file - I get an error like this - `Benchmarker.dll { BENCHMARK | TEST } BENCHMARK - Benchmark dictionary enumeration methods. TEST - Display results of dictionary enumeration methods.` so I assume I am missing something. – dragons Apr 29 '20 at 21:44
  • From the project directory, I used `dotnet run -c release --framework netcoreapp3.1 -- benchmark` (note the space between `--` and `benchmark`, which is a parameter for the program itself). You need to pass "`benchmark`" otherwise it just displays that usage text. By the way, for curiosity's sake I will be adding a benchmark for `GetEnumerator()` and updating the code and results. – Lance U. Matthews Apr 29 '20 at 21:47
  • I am running bunch of things on my mac in multiple desktops so might be slower because of all those processes I have @BACON. – dragons Apr 29 '20 at 22:16
  • That was my thought, too, if you've got other applications running at the same time. On Windows 10 I closed all applications and changed the power profile to "High performance" before running the benchmark in `Command Prompt`. At only 10 elements it really doesn't matter what approach you might use, although BenchmarkDotNet should still be able to discern any performance differences for even a microbenchmark like that. – Lance U. Matthews Apr 29 '20 at 22:26
  • So in my case I do have only 20 keys which I already know beforehand. Can we use that as an advantage to iterate my dictionary efficiently? Whatever `key` I have in switch block I already know about them. – dragons Apr 29 '20 at 22:33
  • In my results, iterating through a known set of consecutive integer keys still had a small performance hit, though yours showed a small improvement. For the sake of completeness, after adding benchmarks that iterate keys cached in a separate array I find that it's even slower _still_, probably because there's now an additional array access. So, I'm still seeing that `foreach`/`GetEnumerator()` is the fastest way to go. How often are you enumerating this 20-element dictionary, anyways? – Lance U. Matthews Apr 29 '20 at 23:24
  • hey. quick question - since you showed me how to use `BenchmarkDotNet` so I was thinking to use for my other project. I have posted a question [here](https://stackoverflow.com/questions/62010592/generate-random-throughput-from-x-number-of-threads-and-print-performance-number). Do you think it is also possible to achieve what I want there using `BenchmarkDotNet`? – dragons May 25 '20 at 21:04
-1

It would matter only in very extreme cases. The performance draw back in foreach is that you have to write into another variable, which in for you don't. The foreach is basically this:

for(int i = 0, i < something.Length; i++)
{
   var item = something[i]; //which is why you can just use the item from collection
   //your code using the item var...
}
  • Thanks for suggestion. So for this case you think its better to go with for loop? If yes can you provide an example for my dictionary case? Here `values` is `IDictionary` object. I see examples with for loop but was confuse since few folks mention that enumerable will take o(n) so wanted to make sure right way. – dragons Apr 29 '20 at 17:50
  • That depends on what you prefer, but I don't use foreach. Not because it's slower, just 'cause I like it more and its more useful, for example the counter that you get from the `int`. – Filip Gajdušek Apr 29 '20 at 18:01
  • why do you have this check in for loop version - ` if(dictionary.Count>0)` – dragons Apr 29 '20 at 18:12