首页 > 解决方案 > 在迭代字典c#中循环for vs foreach差异

问题描述

我有一个下面的foreach循环来完成这项工作。我很想知道我的以下情况 - 使用for循环而不是foreach循环来解决性能问题会更好吗?

因为我读到for循环比foreach循环快所以我也有点困惑。

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

标签: c#.netperformancedictionaryforeach

解决方案


由于字典没有定义的顺序,接口中缺少任何类型的索引器可能会导致在使用/的情况下IDictionary<>难以迭代。鉴于...foreachGetEnumerator()

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

...由于您知道键包含连续的整数范围,因此您可以使用for循环遍历所有可能的键值...

// 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];

    // ...
}

然而,如果你不能做出这样的假设,它就会变得更加棘手。您可以迭代Keys集合属性以获取每个元素的键...但是该集合也不允许索引,因此您又回到了foreachfor困境开始的地方。但是,如果您坚持使用for,则一种方法是复制Keys数组然后迭代...

// 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];

    // ...
}

当然,在你自己有机会这样做之前CopyTo()会枚举一个完整的时间,这样只会损害性能。Keys

如果您正在使用预先知道的一组固定键,或者您不介意每次字典键更改时都必须维护单独的键集合,那么更好的方法是将键缓存在一个结构中可以索引...

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];

    // ...
}

改用LINQElementAt()方法可能很诱人;毕竟,它很容易使用......

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

    // ...
}

但是,这对性能非常不利ElementAt()只能在输入集合实现时进行索引的特殊情况IList<>,它既不继承Dictionary<>也不IDictionary<>继承自它。否则,对于您尝试检索的每个索引,它都必须从头开始。考虑枚举上面定义的整个 10 元素字典...

| 请求索引 | 枚举元素 | 枚举的总元素 |
|:---------------:|:----------------:| :----------------------------:|
| 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 |

将所有这些加起来,将需要55次枚举才能逐步遍历 10 元素字典!因此,为了通过消除foreach/来提高性能,GetEnumerator()这只会将GetEnumerator()调用转移到幕后并使性能变得更糟

至于这些方法的实际性能差异,这是我得到的结果......

// * 概括 *

BenchmarkDotNet=v0.12.1,操作系统=Windows 10.0.18363.657 (1909/November2018Update/19H2)
Intel Core i7 CPU 860 2.80GHz (Nehalem),1 个 CPU,8 个逻辑核心和 4 个物理核心
.NET Core SDK=3.1.201
  [主机]:.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


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

...从我使用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}");
                }
            }
        }
    }
}

请注意,上面的代码100_000Benchmarks.DictionarySizes属性中省略了,因为它增加了一个多小时的运行时间。

结论:

  • foreach/GetEnumerator()是迭代字典的最快方法。
  • 根据运行时,当您可以对您的键做出一些假设时,使用foror循环充其量会稍微慢一些,但它仍然较慢while
  • 在循环ElementAt()内使用具有糟糕的性能,只有字典越大越慢。for

推荐阅读