【C#】Listと配列に対するforとforeachのアクセス速度の比較

先に結論ですが、全く同じ処理を for と foreach で行った場合、List では for が早い、配列ではほぼ同じ速度になります。

確認環境

  • .NET5 + C#9.0
  • VisualStudio 2019
  • Windows10
  • Cire-i7 3770K + 16GB RAM

Relaseビルドしたバイナリをコンソールから実行して確認してます。

補足:

ランタイムのバージョンや言語バージョン(コンパイラのバージョン)PC環境によって変動します。あくまで記載の環境で実行したらこうなります。

Listのアクセス速度

実装コード

確認コードは以下の通りです。

単純に10万件のリストを作成してそれに対して同一条件でアクセスしてリストの中身の合計数を求めています。

private static void Main(string[] args)
{
    // ループ回数: 10万回
    const int loopCnt = 100000;

    var list = createTestData(loopCnt);

    // 事前に少し処理を走らせて温めておく
    for (int pp = 0; pp < 1000; pp++)
    {
        var sw1 = Stopwatch.StartNew();
        int sum1 = 0;
        for (int i = 0; i < list.Count; i++)
        {
            sum1 += list[i];
        }
        sw1.Stop();

        var sw2 = Stopwatch.StartNew();
        int sum2 = 0;
        foreach (var p in list)
        {
            sum2 += p;
        }
        sw2.Stop();
    }

    // forを先に実行
    {
        var a1 = new List<double>();
        var a2 = new List<double>();
        for (int pp = 0; pp < 1000; pp++) // 計測を1000回繰り返して平均を取る
        {
            GC.Collect();

            var sw1 = Stopwatch.StartNew();
            for (int i = 0; i < list.Count; i++)
            {
            }
            sw1.Stop();
            a1.Add(sw1.Elapsed.TotalMilliseconds);

            GC.Collect();

            var sw2 = Stopwatch.StartNew();
            foreach (var p in list)
            {
            }
            sw2.Stop();
            a2.Add(sw2.Elapsed.TotalMilliseconds);
        }

        Console.WriteLine("case.1");
        Console.WriteLine($"for, {a1.Average():F5}msec");
        Console.WriteLine($"foreach, {a2.Average():F5}msec");
        // case.1
        // for, 0.02406msec
        // foreach, 0.16798msec

    }

    // foreachを先に実行
    {
        var a2 = new List<double>();
        var a1 = new List<double>();
        for (int pp = 0; pp < 1000; pp++) // 計測を1000回繰り返して平均を取る
        {
            GC.Collect();

            var sw2 = Stopwatch.StartNew();
            int sum2 = 0;
            foreach (var p in list)
            {
                sum2 += p;
            }
            sw2.Stop();
            a2.Add(sw2.Elapsed.TotalMilliseconds);

            GC.Collect();

            var sw1 = Stopwatch.StartNew();
            int sum1 = 0;
            for (int i = 0; i < list.Count; i++)
            {
                sum1 += list[i];
            }
            sw1.Stop();
            a1.Add(sw1.Elapsed.TotalMilliseconds);
        }

        Console.WriteLine("case.2");
        Console.WriteLine($"foreach, {a2.Average():F5}msec");
        Console.WriteLine($"for, {a1.Average():F5}msec");
        // case.2
        // foreach, 0.17115msec
        // for, 0.12162msec
    }
}

// テスト時に使用するデータの作成
private static List<int> createTestData(int count)
{
    IEnumerable<int> f()
    {
        for (int i = 0; i < count; i++)
        {
            yield return i;
        }
    }
    return f().ToList();
}

計測結果

1000回計測した平均は以下の通りでした。

# for foreach
case.1 0.02406msec 0.16798msec
case.2 0.12162msec 0.17115msec

(1) と (2) で少し時間が異なりますが、for の方が早いです。

配列のアクセス速度

確認コード

次は配列のアクセス速度の比較です。

先ほどのコードとほぼ変わりないですが計測コードと結果は以下の通りです。

private static void Main(string[] args)
{
    // ループ回数: 10万回
    const int loopCnt = 100000;

    var list = createTestData(loopCnt);

    // 事前に少し処理を走らせて温めておく
    for (int pp = 0; pp < 1000; pp++)
    {
        var sw1 = Stopwatch.StartNew();
        int sum1 = 0;
        for (int i = 0; i < list.Length; i++)
        {
            sum1 += list[i];
        }
        sw1.Stop();

        var sw2 = Stopwatch.StartNew();
        int sum2 = 0;
        foreach (var p in list)
        {
            sum2 += p;
        }
        sw2.Stop();
    }

    // forを先に実行
    {
        var a1 = new List<double>();
        var a2 = new List<double>();
        for (int pp = 0; pp < 1000; pp++) // 計測を1000回繰り返して平均を取る
        {
            GC.Collect();

            var sw1 = Stopwatch.StartNew();
            for (int i = 0; i < list.Length; i++)
            {
            }
            sw1.Stop();
            a1.Add(sw1.Elapsed.TotalMilliseconds);

            GC.Collect();

            var sw2 = Stopwatch.StartNew();
            foreach (var p in list)
            {
            }
            sw2.Stop();
            a2.Add(sw2.Elapsed.TotalMilliseconds);
        }

        Console.WriteLine("case.1");
        Console.WriteLine($"for, {a1.Average():F5}msec");
        Console.WriteLine($"foreach, {a2.Average():F5}msec");
        // case.1
        // for, 0.02476msec
        // foreach, 0.02483msec

    }

    // foreachを先に実行
    {
        var a2 = new List<double>();
        var a1 = new List<double>();
        for (int pp = 0; pp < 1000; pp++) // 計測を1000回繰り返して平均を取る
        {
            GC.Collect();

            var sw2 = Stopwatch.StartNew();
            foreach (var p in list)
            {
            }
            sw2.Stop();
            a2.Add(sw2.Elapsed.TotalMilliseconds);

            GC.Collect();

            var sw1 = Stopwatch.StartNew();
            for (int i = 0; i < list.Length; i++)
            {
            }
            sw1.Stop();
            a1.Add(sw1.Elapsed.TotalMilliseconds);
        }

        Console.WriteLine("case.2");
        Console.WriteLine($"foreach, {a2.Average():F5}msec");
        Console.WriteLine($"for, {a1.Average():F5}msec");
        // case.2
        // foreach, 0.02397msec
        // for, 0.02397msec
    }
}

// テスト時に使用するデータの作成
private static int[] createTestData(int count) // ☆ここを配列にしている
{
    IEnumerable<int> f()
    {
        for (int i = 0; i < count; i++)
        {
            yield return i;
        }
    }
    return f().ToArray();
}

計測結果

1000回計測した平均は以下の通りでした。

# for foreach
case.1 0.02476msec 0.02483msec
case.2 0.02397msec 0.02397msec

処理自体が List よりだいぶ高速です。

若干安定しませんが、今度は両方ともだいたい同じくらいの速度になりました。

結論

結論です。

  • List は for が高速
  • 配列は同じくらいの速度

foreach のイテレーターは .NET Framework の頃はもっと遅かった気がします。最近早い?のかもしれません。

最後に

で、ここまで長々と書いておいてなんですが、この後、実際はループの中に書く処理の方が何倍も重たいでしょうし、どう書いたところでループの処理コストなんて全体の中で本当に極小のため、こんな事を気にして実装すること自体がナンセンスかもしれませんね。

この結果に囚われずに読みやすい方で書いたらいいと思います。