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

先に結論ですが、全く同じ処理を for と foreach で行った場合、Listではfor、配列では foreach の方が早いです。

以下、検証内容です。

確認環境

  • .NET Core3.1
  • VisualStudio 2019
  • Windows10
  • Cire-i7 3770K + 16GB RAM

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

Listのアクセス速度

実装コード

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

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

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

    var list = createTestData(loopCnt);

    // キャッシュが効いて結果が不公平にならないように
    // 同じ処理を計測せず事前に1000回走らせておく
    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();
        }
    }

    {
        List<double> a1 = new List<double>();
        List<double> a2 = new List<double>();
        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();
            a1.Add(sw1.Elapsed.TotalMilliseconds);

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

        Console.WriteLine($"for, {a1.Average()}msec");
        Console.WriteLine($"foreach, {a2.Average()}msec");
        // > for, 0.16245250000000092msec
        // > foreach, 0.24426649999999864msec
    }
}

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
0.1624525msec 0.2442665msec

for の方が早い。

配列のアクセス速度

確認コード

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

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

public static void Main(string[] args)
{
    // ループ回数
    int loopCnt = 100000;

    var list = createTestData(loopCnt);

    // キャッシュが効いて結果が不公平にならないように
    // 同じ処理を計測せず事前に1000回走らせておく
    // for(... 省略

    {
        List<double> a1 = new List<double>();
        List<double> a2 = new List<double>();
        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();
            a1.Add(sw1.Elapsed.TotalMilliseconds);

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

        Console.WriteLine($"for, {a1.Average()}msec");
        Console.WriteLine($"foreach, {a2.Average()}msec");
        // > for, 0.13522820000000016msec
        // > foreach, 0.053829400000000284msec
    }
}

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
0.1352282msec 0.0538294msec

配列の方がListよりだいぶ処理自体が高速ですね。

今度は foreach の方が倍以上早いです。

速度がListと配列で逆転する理由

どうして各々の foreach でこんなに速度差が出る(しかも逆転する)のか少し内容を見ていきたいと思います。

まず、以下コンパイルしたときに.NETのコンパイラーが追加したコードです。

List の foreach と配列の foreach だとコンパイラーが生成するコードが見かけ以上に全然違います。

// ★Listの場合、、、、

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

// ↓ ★List の場合 foreach は以下のように展開される

Stopwatch stopwatch2 = Stopwatch.StartNew();
int num2 = 0;
List<int>.Enumerator enumerator = list.GetEnumerator();
try
{
    while (enumerator.MoveNext())
    {
        int current = enumerator.Current;
        num2 += current;
    }
}
finally
{
    ((IDisposable)enumerator).Dispose();
}
stopwatch2.Stop();

// ↓ ★配列の場合 foreach は以下のように展開される

Stopwatch stopwatch2 = Stopwatch.StartNew();
int num2 = 0;
int[] array2 = array;
foreach (int num3 in array2)
{
    num2 += num3;
}
stopwatch2.Stop();
Console.WriteLine(string.Format("{0}msec, {1}", stopwatch2.Elapsed.TotalMilliseconds, num2));

イテレーターが MoveNext() するのは遅くて当然といった感じです。

では配列はどうかというと、更に配列に対して for するのと foreach は IL を見ると以下のようになります。

IL_0094: ldc.i4.0
IL_0095: stloc.s 14
IL_0097: ldc.i4.0
IL_0098: stloc.s 17
// sequence point: hidden
IL_009a: br.s IL_00ab
// loop start (head: IL_00ab)
    IL_009c: ldloc.s 14
    IL_009e: ldloc.0
    IL_009f: ldloc.s 17
    IL_00a1: ldelem.i4
    IL_00a2: add
    IL_00a3: stloc.s 14
    IL_00a5: ldloc.s 17
    IL_00a7: ldc.i4.1
    IL_00a8: add
    IL_00a9: stloc.s 17

    IL_00ab: ldloc.s 17
    IL_00ad: ldloc.0
    IL_00ae: ldlen
    IL_00af: conv.i4
    IL_00b0: blt.s IL_009c
// end loop


IL_00d7: ldc.i4.0
IL_00d8: stloc.s 16
IL_00da: ldloc.0
IL_00db: stloc.s 7
IL_00dd: ldc.i4.0
IL_00de: stloc.s 8
// sequence point: hidden
IL_00e0: br.s IL_00f6
// loop start (head: IL_00f6)
    IL_00e2: ldloc.s 7
    IL_00e4: ldloc.s 8
    IL_00e6: ldelem.i4
    IL_00e7: stloc.s 19
    IL_00e9: ldloc.s 16
    IL_00eb: ldloc.s 19
    IL_00ed: add
    IL_00ee: stloc.s 16
    // sequence point: hidden
    IL_00f0: ldloc.s 8
    IL_00f2: ldc.i4.1
    IL_00f3: add
    IL_00f4: stloc.s 8

    IL_00f6: ldloc.s 8
    IL_00f8: ldloc.s 7
    IL_00fa: ldlen
    IL_00fb: conv.i4
    IL_00fc: blt.s IL_00e2
// end loop

配列のforeachは i++ の足し算をしていない分早いようです(これは自分もだいぶ予想外でした…すごいですね

という事で配列にシーケンシャルアクセスする場合 foreach がいい感じという話になりました。

おまけ

悪い計測例

同一の考察を検索したときに参考にしてはいけない例を見たので一応注意しておこうと思います。以下ネットで見た悪い例です。

Google検索結果で"c# list foreach vs for performance"と検索して検索結果第2位のサイトです。

以下記事ではforeachの方が早いと結論していますが、記事中のコードではforeachの方がリストに対するアクセス回数が1000倍違うのにforeachの方が10倍早いと結論しています。

確かに「このコードでは」早いでしょうね。以下問題点をコメントで指摘しています。

// 悪い計測例
List<string> testlist = // 各要素に"a"が入っている100000件のリスト
string buf;
for (var i = 0; i < testlist.Count; i++)
{
    for (var cnt = 0; cnt < 1000; cnt++)
    {
        buf = testlist[i]; // 合計で1億回リストにインデックスアクセスしている
    }
}

foreach (var i in testlist)
{
    for (var cnt = 0; cnt < 1000; cnt++)
    {
        buf = i; // 合計で10万回リストにアクセスしている
    }
}

ちょっと何がしたいのか不明ですが各々のループの目的と条件が全く異なるので公平に計測できていません。

上記を公平に取り扱って計測するなら for のループは以下のように訂正したほうが良いでしょう。

// 悪い計測例
List<string> testlist = // 各要素に"a"が入っている100000件のリスト
string buf;

// ★(1)
// foreachの例とアクセス回数が揃うよう
// にいったんバッファに入れておくための一時変数
string _buf = "";
for (var i = 0; i < testlist.Count; i++)
{
    // ★(2)
    // ここでバッファーに値を入れておく
    _buf = testlist[i];
    for (var cnt = 0; cnt < 1000; cnt++)
    {
        // ★(3)
        // これで10万回にになるので同じ回数比較になる
        buf = _buf;
    }
}

foreach (var i in testlist)
{
    for (var cnt = 0; cnt < 1000; cnt++)
    {
        buf = i; // こっちは変更なし
    }
}

// > for Loop     : 144 ms ★foreachより早い
// > foreach Loop : 177 ms
// > ForEach Loop : 168 ms

こういったコードを公開するときはよく考えてからコード書いたほうがいいですね(自戒