ノートの端の書き残し

UnityやらC#やら。設計が得意かもしれない。

List, Array, ForEachの速度比較(BenchmarkDotNetで遊ぶ)

List.ForEach()よりはforeachループ、
Listのforeachループよりはforループ、
foreach回すだけならToListしなくても、

といったことは知ってますが、実際のところどのくらいどれが有利なの?というのをちゃんと知っておかないと、他人に説明できなくて困る。
というわけでBenchmarkDotNetを使って調べてみましょう。 BenchmarkDotNetの使い方を調べてみたら全く同じものを検証してるのが見つかりますが、自分でやるのに意味があるんだと言い聞かせ。

以下コード

using System.Collections.Generic;
using System.Linq;
using BenchmarkDotNet.Running;
using BenchmarkDotNet.Attributes;

namespace BenchmarkTest
{
    class Program
    {
        static void Main(string[] args)
        {
            BenchmarkRunner.Run<TestClass>();
        }
    }
    [MemoryDiagnoser]
    public class TestClass
    {
        IEnumerable<int> intEnum;

        [GlobalSetup]
        public void Setup()
        {
            intEnum = Enumerable.Range(0, 1000);
        }

        [Benchmark]
        public void ToListForEach()
        {
            intEnum.ToList().ForEach(i => { });
        }

        [Benchmark]
        public void ToListForEachLoop()
        {
            foreach (var i in intEnum.ToList()) { }
        }

        [Benchmark]
        public void ToListCastForEachLoop()
        {
            IEnumerable<int> l = intEnum.ToList();
            foreach (var i in l) { }
        }

        [Benchmark]
        public void ToListForLoop()
        {
            var list = intEnum.ToList();
            for (var i = 0; i < list.Count; i++) { }
        }

        [Benchmark]
        public void ToArrayForEachLoop()
        {
            foreach (var i in intEnum.ToArray()) { }
        }

        [Benchmark]
        public void ToArrayForLoop()
        {
            var array = intEnum.ToArray();
            for (var i = 0; i < array.Length; i++) { }
        }

        [Benchmark]
        public void ForEachLoop()
        {
            foreach (var i in intEnum) { }
        }
    }
}
Method Mean Error StdDev Gen 0 Gen 1 Gen 2 Allocated
ToListForEach 3.904 μs 0.0611 μs 0.0542 μs 1.2894 - - 4,056 B
ToListForEachLoop 3.931 μs 0.0477 μs 0.0446 μs 1.2894 - - 4,056 B
ToListCastForEachLoop 8.409 μs 0.0894 μs 0.0837 μs 1.2970 - - 4,096 B
ToListForLoop 2.079 μs 0.0197 μs 0.0164 μs 1.2894 - - 4,056 B
ToArrayForEachLoop 1.261 μs 0.0146 μs 0.0130 μs 1.2817 - - 4,024 B
ToArrayForLoop 1.052 μs 0.0119 μs 0.0111 μs 1.2817 - - 4,024 B
ForEachLoop 5.068 μs 0.0413 μs 0.0386 μs 0.0076 - - 40 B
  • forechループの速度は、配列 > List > IEnumerable
  • ForEachは速度は実は悪くない(内部的にはforループ) referencesource.microsoft.com けどアロケーションが段違いで、大量に回す場合はこっちのほうがよっぽど影響大きい。
  • ToArrayのループも、配列なのでListよりは速いとは思っていたものの、ここまで早いとは思わなかった。
  • IEnumerableがToListしたやつよりも遅いのはMoveNextのパフォーマンスですかね。ListのGetEnumeratorが返すEnumeratorはList用に最適化されてますので、Rangeの返すクラス(Rangeはただのyield returnなのでコンパイラ生成のEnumerator)より優秀なんでしょう。
  • ToListした上でIEnumerableとして扱うのは良くないとこ取りで最悪。Enumeratorのボックス化もある。

正直ToListForEachはもっと散々な結果になると思っていましたので、意外ですね。なんか~.ToList.ForEachって記述を見かけて、この結果を見せて納得してもらおうと思っており、全てにおいて最悪!みたいなのをちょっと期待してたんですけどね。
この速度差とアロケーション差ならやはりForEachが優先されるケースは無いので十分ではありますか。