为啥 ToArray 性能有这样的行为,.Net Core 3.1?

Posted

技术标签:

【中文标题】为啥 ToArray 性能有这样的行为,.Net Core 3.1?【英文标题】:Why ToArray performance has such behavior, .Net Core 3.1?为什么 ToArray 性能有这样的行为,.Net Core 3.1? 【发布时间】:2020-05-05 14:05:44 【问题描述】:

我附上了内存测试

我编辑了问题

我在IEnumerable<int> 上对ToArrayToList 方法进行了基准测试,我在图形上看到了从530 到80 万的坑。 我固定了我的基准代码:

 [MarkdownExporter, AsciiDocExporter, htmlExporter, CsvExporter, RPlotExporter, PlainExporter] [MemoryDiagnoser]
    public class IntBenchmarks
    
        private IEnumerable<int> EnumerableInts;


        [Params(
            // 10000 ... 1000000
        )]
        public int _count;

        public IntBenchmarks()
        
            EnumerableInts = GetEnumerableInts();
        


        private IEnumerable<int> GetEnumerableInts()
        
            for (var i = 0; i < _count; i++)
            
                yield return 1;
            
        


        [Benchmark]
        public void ToArrayInt()
        
            var r = EnumerableInts.ToArray();
        


        [Benchmark]
        public void ToListInt()
        
            var r = EnumerableInts.ToList();
        

    

另外,我知道当_count 等于 530000 时,内存分配(有新数组的内存)。我很感兴趣为什么分配内存时性能更好。我有 IEnumerable 的 class、struct、int string 的基准,只有 int 的 IEnumerable 有这样的行为

我反复检查过

内存测试:

【问题讨论】:

.NET Framework 还是 .NET Core? 我很惊讶ToArray 更快。我敢打赌ToList 会更快,因为它涉及的步骤更少。 (Is it better to call ToList() or ToArray() in LINQ queries?)。您是否也在 .NET Framework 上对其进行了测试? ToList 在 .net core 和 .net framework 中的实现是不同的。我没有在.net 框架上测试它,但我知道ToList 在那里更好,因为ToArray 方法在内部调用ToList。但是.net 核心开发人员重写了ToArray,现在速度更快了。 @TheodorZoulias 我可以看到还有13万和26万起的小坑。看来坑是按照内部缓冲区的resize模式(变满时大小翻倍)。 是的,我同意,但我不明白为什么?这可能是 JIT 技巧@TheodorZoulias 【参考方案1】:

在一个真正的 IEnumerable 上,它不能被转换为其他任何东西,你通过使用 yield return 迭代器方法强制执行的操作,ToArray 正在调用 ToList,不是因为 ToList 更快,但它更灵活,它已准备好处理未知的数据计数。我们必须避免枚举枚举两次,例如,如果使用 Count() 扩展名会发生什么。

ToArray() 会更快 - 如果您提前知道元素的数量。如果源是 ICollection,就会出现这种情况——这实际上是 ICollection 支持此功能的唯一想法——知道 Advance 中的元素数量。

您的测量特定于纯迭代器(具有收益回报)。如果 IEnumerable 被测试为 true,为 ArrayIListICollection,则相当强制转换,并使用本机实现 List.CopyToArray.CopyTo。它们不枚举 IEnumerable。 只是强调一下:list.ToList() 和 list.ToArray() 或 int[].ToList() 或 int[].ToArray() 不会显示这种行为

我无法解释 .NET/Core 的可能差异。

【讨论】:

您描述了 .net 框架行为。 .net 核心 ToArray 方法不调用 ToList。我的问题是为什么我们的坑从 530 到 80 万。谢谢 是的,我知道,这不是答案。但评论太多了,重要的是要宣布您不是在衡量一般行为,而只是在 MoveNext 真正枚举的情况下。我只检查了源代码,在我看来并没有什么不同。 你看了吗source code?@Holger @EvgeniyTerekhin 是的,source.dot.net/#System.Linq/System/Collections/Generic/… EnumerableHelpers.ToArray 正是我所描述的。 (添加了关于字节长度的另一个答案)。我们只是将想法的力量结合起来。【参考方案2】:

EnumerableInts 中的项目数未知。它只是一个具有任意数量项目的可枚举。因此,当您调用ToArray() 时,运行时必须分配一个大小未知的数组。然后它开始从枚举器中复制项目。一旦达到数组长度,它将重新分配一个长度为两倍的新数组。见EnumerableHelpers.cs(76)

枚举器返回的项目数越多,分配的临时数组就越多。您的基准测试中的坑是由 Garbage Collector 造成的,收集不再引用的临时数组。

【讨论】:

以上是关于为啥 ToArray 性能有这样的行为,.Net Core 3.1?的主要内容,如果未能解决你的问题,请参考以下文章

为啥新的 Java 8 流在 toArray 调用上返回一个对象数组?

诠释 x = 10; x += x--;在.Net - 为啥?

将 .ToArray() 放入 from/in [重复] 时,LINQ 查询性能不佳

为啥将 VB.NET 代码迁移到 C# 时,for 循环的行为会有所不同?

.toArray(new MyClass[0]) 还是 .toArray(new MyClass[myList.size()])?

为啥 python 对变量有这种行为?