【问题标题】:Weird performance behavior of .NET dictionary insertion.NET 字典插入的奇怪性能行为
【发布时间】:2021-03-04 14:30:53
【问题描述】:

我有两个具有不同值类型的字典:Dictionary<int, string[]>Dictionary<int, int[]>。假设我们在循环中生成随机数组并将它们插入到字典中(在 C# 中)。

var d1 = new Dictionary<int, string[]>();
var d2 = new Dictionary<int, int[]>();
var sw = Stopwatch.StartNew();
for (int i = 0; i < 40000000; i++)
{
    string[] sarr = new string[10];
    for (int j = 0; j < 10; j++)
    {
        sarr[j] = j.ToString();
    }
    int[] iarr = new int[10];
    for (int j = 0; j < 10; j++)
    {
        iarr[j] = j;
    }
    d1[i] = sarr; // (1)
    d2[i] = iarr; // (2)
}
sw.Stop();

注意 for 循环的最后两行。当我运行上面的代码时,我的机器上大约需要 13.9 秒。现在,当我只注释掉 (1) 时,大约需要 13.7 秒。如果我只注释掉 (2),那么大约需要 20 秒。换句话说,通过删除 (2) 它变得慢得多!我重复了多次,我可以确认行为是一致的。

谁能解释一下这怎么可能?

我做这个实验是因为我注意到插入string[] 比插入int[] 慢,即使我在两个字典中使用相同的键。我想知道为什么插入string [] 也会比插入int[] 慢。

所以我的问题是双重的:(1)为什么从上面的代码中删除一行会使事情变慢,(2)为什么插入string[]比插入int[]慢?


仅供参考,我使用的是最新的 .NET 5 (5.0.103)。我在 Windows 和 Linux 上都尝试了代码,并且行为是相同的。使用调试或发布模式时,我始终看到同样的问题。


当我区分注释掉的版本和原始版本的 IL 时,注释掉的版本没有按预期调用字典的 set_Item 函数。其他的都差不多。

IL_0079: ldloc.1      // dictionary2
IL_007a: ldloc.3      // key
IL_007b: ldloc.s      numArray
IL_007d: callvirt     instance void class [System.Collections]System.Collections.Generic.Dictionary`2<int32, int32[]>::set_Item(!0/*int32*/, !1/*int32[]*/)
IL_0082: nop

例如,当我注释掉(2)时,上面的部分被删除了。


为了帮助重现这个问题,我使用 Benchmark.NET 创建了一个简单的 repo:https://github.com/sangkilc/TestDictionary。在这个 repo 中,我减少了迭代次数(从 40M 到 4M),因为它花费的时间太长了。

在我的机器上,结果是:

.NET Core SDK=5.0.103
  [Host]     : .NET Core 5.0.3 (CoreCLR 5.0.321.7212, CoreFX 5.0.321.7212), X64 RyuJIT
  DefaultJob : .NET Core 5.0.3 (CoreCLR 5.0.321.7212, CoreFX 5.0.321.7212), X64 RyuJIT


|   Method |    Mean |    Error |   StdDev |
|--------- |--------:|---------:|---------:|
| TestBoth | 1.269 s | 0.0222 s | 0.0208 s |
|  TestOne | 1.381 s | 0.0257 s | 0.0241 s |

根据@TheodorZoulias 的观察,如果我将d2 修改为二维数组,那么差异会变得更加显着:

|   Method |    Mean |    Error |   StdDev |
|--------- |--------:|---------:|---------:|
| TestBoth | 1.137 s | 0.0195 s | 0.0163 s |
|  TestOne | 1.373 s | 0.0246 s | 0.0345 s |

【问题讨论】:

  • 我可以确认我在调试和发布模式下看到一致的行为。
  • 您是否尝试查看生成的 IL 以找出差异?
  • 无法重现。在带有 .NET 5 的 Linqpad 上进行了尝试,开启和关闭优化
  • @XouDo 你倒退了。在 GC 运行时保持对象活动需要 CPU 资源(既要遍历它们的内存以查找其他引用的对象,也可以复制它们的数据),发生收集时不活动的对象不消耗任何资源。跨度>
  • 如果你把var d2 = new Dictionary&lt;int, int[]&gt;()换成一个简单的数组,这个神秘的行为就更加夸张了:var d2 = new int[40000000][]

标签: c# .net performance dictionary


【解决方案1】:

这不是回答,只是想炫耀一些图片

我在 Release 中为您的两个场景编译了代码:

  • 同时使用 (1) 和 (2)
  • 只有 (1) 个

注意我删除了Stopwatch相关代码,因为我们只对Dictionary感兴趣。

我的机器上有 dotTrace,所以我得到了一些分析结果(逐行)。

对于两者场景:

按线程树:

按方法:

仅适用于 (1) 场景:

按线程树:

按方法:

由于您提到模式是一致的,所以我只运行了一次分析。

从结果中,我们可以看出与Dictionary 相关的函数并不是所谓“奇怪的性能行为”的主要贡献者,GC可能是。

【讨论】:

  • 您展示的性能细分显然支持 GC 问题;大约需要 7%。在看到很多人的 cmets 之后,我认为问题显然是由于 GC 造成的,我将选择这个作为答案。还有一点需要注意:如果我减少循环迭代的次数,奇怪的行为就会消失。所以更少的垃圾,更少的性能下降。谢谢!
猜你喜欢
  • 2021-03-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-11-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多