分配Dictionary对象有多贵?

时间:2016-06-03 07:00:39

标签: c# performance parallel-processing

最近我发现将Dictionary对象分配给自定义对象属性时出现问题。我有一个函数调用400,000并在函数内部尝试将字典对象分配给我的自定义对象的属性。当我计算时间时,我发现差异大约为1500毫秒:

CustomClass[] customObjects = new CustomClass[400000];
for (int indx = 0; indx < 400000; indx++)
{
    customObjects[indx] = new CustomClass
    {
        Prop1 = new Dictionary<long, Class1>{ { 1, new Class1 { Prop1 = 2 , Prop2 = "Some Text 1" , Prop3 = DateTime.Now  } }, 
                                        { 2, new Class1 { Prop1 = 3 , Prop2 = "Some Text 2" , Prop3 = DateTime.Now } } 
        }
    };
}


var sw = Stopwatch.StartNew();
Parallel.For(0, 400000, (indx) =>
{
    var myNewDic = new Dictionary<long, Class1>() { { 1, new Class1 { Prop1 = 2 , Prop2 = "Some Text 1" , Prop3 = DateTime.Now } }, 
                                        { 2, new Class1 { Prop1 = 3, Prop2 = "Some Text 2" , Prop3 = DateTime.Now  } } };

    customObjects[indx].Prop1 = myNewDic; // this one is slower

});

sw.Stop();

Console.WriteLine("First try has been finished with: {0} ms", sw.ElapsedMilliseconds);

sw = Stopwatch.StartNew();
Parallel.For(0, 400000, (indx) =>
{
    var myNewDic = new Dictionary<long, Class1>() { { 1, new Class1 { Prop1 = 2 , Prop2 = "Some Text 1" , Prop3 = DateTime.Now } }, 
                                        { 2, new Class1 { Prop1 = 3, Prop2 = "Some Text 2" , Prop3 = DateTime.Now  } } };

    foreach (var key in myNewDic.Keys) // this one is faster
    {
        customObjects[indx].Prop1[key].Prop1 = myNewDic[key].Prop1;
    }
});

sw.Stop();

Console.WriteLine("Second try has been finished with: {0} ms", sw.ElapsedMilliseconds);

Console.ReadLine();

结果:

Result:

我的假设是分配字典应该只更改内存中的指针而垃圾收集器应该销毁旧值,但似乎还有更多的东西。

有人有意见吗?

2 个答案:

答案 0 :(得分:3)

免责声明:我所描述的所有内容都是关于C#和.NET的实现细节,并且可以随时使用更新的.NET运行时和/或C#版本进行更改。这适用于Windows上的当前Microsoft.NET运行时。这不应该是令人惊讶的 - 毕竟我们正在谈论性能优化,这里的问题是由非最佳垃圾收集和内存分配机制引起的,这可能会在未来得到改进:)< / p>

你的两个例子之间的主要原因是&#34; die&#34;分配。由于您处理了40万个项目,因此可以非常安全地假设customObjects数组中的字典已经位于第2代堆中,并且位于其他已分配对象的中间位置。另一方面,新创建的字典不能在单个循环迭代中存活,并且很可能在第0代堆中分配。

这有两个主要影响:

  • Gen 0堆更小,更快地遍历和收集。只要您没有分配足够的持久性内存来跨越阈值,您的所有分配和集合都将处理Gen 0,这要快得多。这仍然不像堆栈集合那么便宜,但它在你通常的应用程序中非常接近。
  • 当你从堆中间收集一些内存时,需要移动它上面的每个对象,以便没有&#34;自由空间&#34;在堆中。这是因为除了LoH之外,.NET堆分配总是从堆顶部完成,与堆栈相同。这使得分配速度非常快,但这意味着在释放内存时必须压缩堆以确保可用内存实际可用。毋庸置疑,如果你的堆很大,移动一半的堆是非常昂贵的。

您的示例实际上并没有在我的计算机上产生有意义的差异 - 这很可能是因为它只分配了相对少量的内存,这使得整个堆很好地适应处理器缓存。移动更多的内存意味着更多的工作。

基本上,你的方法通过使用几乎可能的最差分配策略来杀死分代垃圾收集器 - 用短寿命对象替换长寿命对象,这将被提升到更高代,直到它再次被替换为一个短暂的物体。你的问题不在于作业(除非你有一些代码隐瞒了我们:)),你必须收集旧词典。

<强>更新

好的,我尝试通过分析器运行您的最新样本。在分析器下,快速版本需要大约8.5秒,慢速版本大约需要9.5秒。这并不奇怪 - 完整的分配/调用分析需要相当多的工作,并且往往会删除或隐藏其他一些争用来源。

那么,我们从内存分析中看到了什么?

  • 慢速版本中对象的生命周期或多或少地遍及整个应用程序。在快速版本中,只有不能在整个运行中存活的对象的单个数字存活多个集合。
  • 在这两个版本中,几乎所有活着的对象都在Gen 1/2中(两个堆不容易区分,但它对我们的目的来说并不是很重要)。这主要是因为Gen 0堆的大小受到严格限制。
  • 在慢速版本中,几乎所有堆内存(总共246 MB中的234 MB)都已重新定位。在快速版本中,只有几千字节被重新安置。这是一个大问题 - 它意味着慢速版本像疯了一样压缩堆,而快速版本几乎不需要进行任何压缩。在慢速版本中,这涉及移动总共大约四百万个对象 - 在快速版本中,六个百个。这包括平均每次移动Class1个对象几次。
  • 慢速版本执行了8个Gen 0堆集合,而快速版本执行了22个。这是一个巨大的差异 - 它显示了快速版本适合代际GC的好多少。虽然慢速版本的3 Gen 2只比快速版本的2个版本多一个,但我们已经看到快速版本的Gen 2系列与慢速版本相比几乎是微不足道的。实际上,前两个Gen 2集合不涉及集合 - 它们都发生在设置阶段,只是作为我们正在进行的所有分配的副作用。
  • 两个版本之间的差异在单线程版本中要小得多。同样,这并不奇怪 - 在删除DateTime.Now(否则占用CPU时间的80-90%,这对我们的测试不切实际)之后,工作负载主要由GC主导。当GC运行时,其他线程都没有 - 由于GC在快速版本中花费的时间少得多,因此并行版本更快。在我没有分析的测试中,非并行版本的速度大约是慢速的一半,而并行版本的速度几乎是慢速的五倍(在我的四核机器上)。

只运行一个简单的并发分析通常会使慢速版本的速度大约是快速版本的两倍,并且您可以轻松地看到GC的责任 - 两个版本之间的差异很容易用肉眼看到。在慢速版本中,GC会冻结所有线程十次左右,而且还有很少的时间可以完成任何工作。在快速版本中,只有一个这样的冻结(当然,您的结果可能会有所不同),总阻塞时间约为5%。

这一切的要点是:确保你的长寿物品长寿,你的短命物体尽快死亡。您可以做的最糟糕的事情是定期用新创建的对象替换(逻辑上)长寿命对象。

答案 1 :(得分:0)

该行:customObjects[indx].Prop1 = myNewDic;在堆上创建一个未被引用的对象,必须进行垃圾回收。第二种情况只是创建一个本地对象并将值复制到已经编译的实例变量。因此没有必要垃圾收集任何东西。

<强>证明:

如果您在for循环之外初始化myNewDic,则行customObjects[indx].Prop1 = myNewDic;会更快,因为您始终使用相同的对象。