【发布时间】:2019-03-08 20:00:51
【问题描述】:
鉴于提高一段代码性能的任务,我遇到了以下现象。我在泛型队列中有大量引用类型,我正在逐个删除和处理元素,然后将它们添加到另一个泛型集合中。
似乎元素越大,将元素添加到集合中所需的时间就越多。
为了将问题缩小到代码的相关部分,我写了一个测试(省略元素的处理,只做插入):
class Small
{
public Small()
{
this.s001 = "001";
this.s002 = "002";
}
string s001;
string s002;
}
class Large
{
public Large()
{
this.s001 = "001";
this.s002 = "002";
...
this.s050 = "050";
}
string s001;
string s002;
...
string s050;
}
static void Main(string[] args)
{
const int N = 1000000;
var storage = new List<object>(N);
for (int i = 0; i < N; ++i)
{
//storage.Add(new Small());
storage.Add(new Large());
}
List<object> outCollection = new List<object>();
Stopwatch sw = new Stopwatch();
sw.Start();
for (int i = N-1; i > 0; --i)
{
outCollection.Add(storage[i];);
}
sw.Stop();
Console.WriteLine(sw.ElapsedMilliseconds);
}
在测试机器上,使用 Small 类,运行大约需要 25-30 ms,而使用 Large 需要 40-45 ms。 我知道 outCollection 必须不时增长才能存储所有项目,因此有一些动态内存分配。但给定初始集合大小甚至会使差异更加明显:小型对象为 11-12 毫秒,大型对象为 35-38 毫秒。
我有点惊讶,因为这些是引用类型,所以我希望这些集合仅适用于对 Small/Large 实例的引用。我已经阅读了 Eric Lippert 的相关 article,并且知道不应将引用视为指针。同时,AFAIK 目前它们被实现为指针,它们的大小和集合的性能应该与元素大小无关。
我决定在这里提出一个问题,希望有人可以解释或帮助我了解这里发生的事情。除了性能提升之外,我真的很好奇幕后发生了什么。
更新: 使用诊断工具分析数据对我没有多大帮助,尽管我不得不承认我不是使用分析器的专家。我将在今天晚些时候收集更多数据以找出瓶颈所在。
GC 的压力当然很大,尤其是Large 实例。但是一旦实例被创建并存储在storage集合中,程序进入循环,就不再触发集合,内存使用也没有显着增加(outCollction已经预分配)。
大部分 CPU 时间当然花在内存分配 (JIT_New) 上,大约 62%,唯一的其他重要条目是 Function Name Inclusive Samples Exclusive Samples Inclusive Samples % Exclusive Samples % Module Name System.Collections.Generic.List`1[System.__Canon]。添加约 7%。
对于 100 万个项目,预分配的 outCollection 大小为 800 万字节(与 storage 的大小相同);人们可能会怀疑集合中存储了 64 位地址。
可能我没有正确使用这些工具,或者没有正确解释结果的经验,但分析器并没有帮助我更接近原因。 如果循环没有触发集合并且它只复制 2 个预分配集合之间的指针,那么项目大小如何导致任何差异?在这两种情况下,缓存命中/未命中率应该大致相同,因为在这两种情况下,循环都是对“地址”列表的迭代。
感谢到目前为止的所有帮助,我会收集更多数据,如果发现任何情况,我会在此处更新。
【问题讨论】:
-
我们使用 Visual Studios 诊断工具收集了哪些事实?
-
阅读您的代码时,我发现您正在将新创建的元素插入到您的列表中。创建元素必然会运行构造函数,
Small类初始化两个字段,而Large类初始化构造函数中的 50 个字段,这是看到差异的一个很好的理由。 -
@D.Foley:我目前正在处理的机器上没有适当的权限,但是当我可以运行分析器时,我会用诊断数据更新我的帖子。 @dumetrulo:如果您检查代码,您可以看到只有
storage集合填充了新创建的元素,并且该部分不包括在测量中。当项目被添加到outCollection时,内存已经分配并且构造函数已经初始化了实例。 -
使用分析器回答性能问题。在互联网上让人们猜测是什么让程序变慢是猜测。使用科学来回答这个经验工程问题。
-
如果被要求猜测,我的第一个想法是 (1) 缓存未命中,以及 (2) 收集压力。这些是可能是错误的假设; 测试它们。
标签: c# performance collections clr