鉴于改善一段代码性能的任务,我遇到了以下现象。我在通用队列中有大量的引用类型集合,我要一一删除并处理元素,然后将它们添加到另一个通用集合中。
似乎元素越大,将元素添加到集合中所花费的时间就越多。
为了将问题缩小到代码的相关部分,我编写了一个测试(省略对元素的处理,仅执行插入操作):
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毫秒,而使用Large类则需要40-45毫秒。 我知道outCollection必须不时地增长以便能够存储所有项目,因此存在一些动态内存分配。但是给定初始收集大小,差异甚至更加明显:小型对象为11-12毫秒,大型对象为35-38毫秒。
我有点惊讶,因为它们是引用类型,所以我期望这些集合只能用于对Small / Large实例的引用。我已经阅读了埃里克·利珀特(Eric Lippert)的相关article,并且知道不应将引用视为指针。同时,AFAIK当前将它们实现为指针,并且它们的大小以及集合的性能应独立于元素大小。
我已经决定在这里提出一个问题,希望有人可以解释或帮助我了解这里发生的事情。除了性能提高之外,我真的很好奇幕后发生的事情。
更新: 尽管必须承认我不是使用探查器的专家,但使用诊断工具来剖析数据并没有多大帮助。今天晚些时候,我将收集更多数据以查找瓶颈所在。
当然,GC上的压力很大,尤其是对于Large
实例。但是一旦创建实例并将其存储在storage
集合中,并且程序进入循环,就不会再触发任何集合,并且内存使用也不会显着增加(outCollction
已经预先分配了)。
当然,大部分CPU时间都花费在内存分配(JIT_New)上,大约62%,唯一其他重要的输入项是功能名称包含样品包含样品包含样品%包含样品%模块名称 System.Collections.Generic.List`1 [System .__ Canon]。添加约7%。
具有100万个项目的预分配outCollection
大小为800万字节(与storage
的大小相同);人们可能会怀疑64位地址存储在集合中。
可能我没有正确使用工具,或者没有经验来正确解释结果,但是探查器并没有帮助我更深入地了解原因。 如果循环未触发集合,并且仅在两个预分配的集合之间复制指针,那么项目大小如何引起任何差异?在这两种情况下,缓存命中率/未命中率应该大致相同,因为在这两种情况下,循环都是在“地址”列表上进行迭代。
感谢到目前为止的所有帮助,我将收集更多数据,如果发现任何问题,请在此处进行更新。
答案 0 :(得分:0)
我怀疑,以上提到的至少一项操作(也许是类型检查)将需要取消引用。然后,许多Small
可能并排放置在堆上,因此共享缓存行的事实可能会造成一定程度的差异(与Large
s相比,肯定有更多n
共享一条缓存行) )。
添加后,您还可以按照分配它们的相反顺序访问它们,从而最大程度地发挥这种优势。