最近我发现将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();
结果:
我的假设是分配字典应该只更改内存中的指针而垃圾收集器应该销毁旧值,但似乎还有更多的东西。
有人有意见吗?
答案 0 :(得分:3)
免责声明:我所描述的所有内容都是关于C#和.NET的实现细节,并且可以随时使用更新的.NET运行时和/或C#版本进行更改。这适用于Windows上的当前Microsoft.NET运行时。这不应该是令人惊讶的 - 毕竟我们正在谈论性能优化,这里的问题是由非最佳垃圾收集和内存分配机制引起的,这可能会在未来得到改进:)< / p>
你的两个例子之间的主要原因是&#34; die&#34;分配。由于您处理了40万个项目,因此可以非常安全地假设customObjects
数组中的字典已经位于第2代堆中,并且位于其他已分配对象的中间位置。另一方面,新创建的字典不能在单个循环迭代中存活,并且很可能在第0代堆中分配。
这有两个主要影响:
您的示例实际上并没有在我的计算机上产生有意义的差异 - 这很可能是因为它只分配了相对少量的内存,这使得整个堆很好地适应处理器缓存。移动更多的内存意味着更多的工作。
基本上,你的方法通过使用几乎可能的最差分配策略来杀死分代垃圾收集器 - 用短寿命对象替换长寿命对象,这将被提升到更高代,直到它再次被替换为一个短暂的物体。你的问题不在于作业(除非你有一些代码隐瞒了我们:)),你必须收集旧词典。
<强>更新强>
好的,我尝试通过分析器运行您的最新样本。在分析器下,快速版本需要大约8.5秒,慢速版本大约需要9.5秒。这并不奇怪 - 完整的分配/调用分析需要相当多的工作,并且往往会删除或隐藏其他一些争用来源。
那么,我们从内存分析中看到了什么?
Class1
个对象几次。DateTime.Now
(否则占用CPU时间的80-90%,这对我们的测试不切实际)之后,工作负载主要由GC主导。当GC运行时,其他线程都没有 - 由于GC在快速版本中花费的时间少得多,因此并行版本更快。在我没有分析的测试中,非并行版本的速度大约是慢速的一半,而并行版本的速度几乎是慢速的五倍(在我的四核机器上)。只运行一个简单的并发分析通常会使慢速版本的速度大约是快速版本的两倍,并且您可以轻松地看到GC的责任 - 两个版本之间的差异很容易用肉眼看到。在慢速版本中,GC会冻结所有线程十次左右,而且还有很少的时间可以完成任何工作。在快速版本中,只有一个这样的冻结(当然,您的结果可能会有所不同),总阻塞时间约为5%。
这一切的要点是:确保你的长寿物品长寿,你的短命物体尽快死亡。您可以做的最糟糕的事情是定期用新创建的对象替换(逻辑上)长寿命对象。
答案 1 :(得分:0)
该行:customObjects[indx].Prop1 = myNewDic;
在堆上创建一个未被引用的对象,必须进行垃圾回收。第二种情况只是创建一个本地对象并将值复制到已经编译的实例变量。因此没有必要垃圾收集任何东西。
<强>证明:强>
如果您在for循环之外初始化myNewDic
,则行customObjects[indx].Prop1 = myNewDic;
会更快,因为您始终使用相同的对象。