元素大小会影响C#集合的性能吗?

时间:2018-10-03 14:38:42

标签: c# performance collections clr

鉴于改善一段代码性能的任务,我遇到了以下现象。我在通用队列中有大量的引用类型集合,我要一一删除并处理元素,然后将它们添加到另一个通用集合中。

似乎元素越大,将元素添加到集合中所花费的时间就越多。

为了将问题缩小到代码的相关部分,我编写了一个测试(省略对元素的处理,仅执行插入操作):

    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位地址存储在集合中。

可能我没有正确使用工具,或者没有经验来正确解释结果,但是探查器并没有帮助我更深入地了解原因。 如果循环未触发集合,并且仅在两个预分配的集合之间复制指针,那么项目大小如何引起任何差异?在这两种情况下,缓存命中率/未命中率应该大致相同,因为在这两种情况下,循环都是在“地址”列表上进行迭代。

感谢到目前为止的所有帮助,我将收集更多数据,如果发现任何问题,请在此处进行更新。

1 个答案:

答案 0 :(得分:0)

怀疑,以上提到的至少一项操作(也许是类型检查)将需要取消引用。然后,许多Small可能并排放置在堆上,因此共享缓存行的事实可能会造成一定程度的差异(与Large s相比,肯定有更多n共享一条缓存行) )。

添加后,您还可以按照分配它们的相反顺序访问它们,从而最大程度地发挥这种优势。