dragons
dragons

Reputation: 619

Looping for vs foreach difference in iterating dictionary c#

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
              }
          }
      }
  }

Upvotes: 0

Views: 4278

Answers (2)

Lance U. Matthews
Lance U. Matthews

Reputation: 16612

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.

Upvotes: 4

Filip Gajdušek
Filip Gajdušek

Reputation: 108

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...
}

Upvotes: -1

Related Questions