【问题标题】:element size influencing C# collection performance?影响 C# 收集性能的元素大小?
【发布时间】: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


【解决方案1】:

怀疑上述至少一项操作(可能是某些类型检查)需要取消引用。然后,许多Smalls 可能在堆上靠得很近,因此共享缓存行可能会造成一定程度的差异(当然,其中更多的人可以共享单个缓存行而不是Larges)。

添加到其中,您也可以按照分配它们的相反顺序访问它们,从而最大限度地提高这种好处。

【讨论】:

  • @Damien_The_Uneliever:谢谢,关于缓存的论点是有道理的,显然更多的Small 实例可以放入缓存而不是Larges。我会尝试找到一种方法来验证它是否会导致差异。以相反的顺序遍历存储是原始代码中保留的内容。朝相反的方向走并没有太大的区别(可能慢 1-2 毫秒),但与平均值的偏差更大。
  • @LittlePilgrim - 任一方向仍然意味着您访问的每个连续项目都是在前一个项目之前/之后分配的项目,因此我们希望它们大致位于同一位置。
猜你喜欢
  • 1970-01-01
  • 2019-10-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-12-09
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多