いろいろ備忘録日記

主に .NET とか Go とか Flutter とか Python絡みのメモを公開しています。

Listをforeachでループした場合とList.ForEachした場合の速度差について (foreach vs List.ForEach, パフォーマンス)


Listの内容をforeachでループした場合とList.ForEachで処理した場合で、どちらが速いかという話題です。
ちょっと自分でも調べましたのでメモメモ。
元々、CodeRush Tips & Tricksの以下の記事を見たのがきっかけでした。


上記記事の中に、「List.ForEachした方がforeachするよりも速いよ」って書いてあったので
サンプル作って確認してみました。

  #region ListForEachDiffSamples-01
  /// <summary>
  /// Listをforeachでループする場合と、List.ForEachする場合の速度差をテスト
  /// </summary>
  public class ListForEachDiffSamples01 : IExecutable
  {
    public void Execute()
    {
      Prepare();
      
      //
      // Listをforeachで処理するか、List.ForEachで処理するかで
      // どちらの方が速いのかを計測。
      //
      // ILレベルで見ると
      //   foreachの場合: callが2つ
      //   List.ForEachの場合: callvirtが1つ
      // となる。
      //
      foreach (var elementCount in new []{1000, 3000, 5000, 10000, 50000, 100000, 150000, 500000, 700000, 1000000})
      {
        Console.WriteLine("===== [Count:{0}] =====", elementCount);
        
        var theList = new List<int>(Enumerable.Range(1, elementCount));

        var watch = Stopwatch.StartNew();
        Sum_foreach(theList);
        watch.Stop();
        Console.WriteLine("foreach:      {0}", watch.Elapsed);

        watch = Stopwatch.StartNew();
        Sum_List_ForEach(theList);
        watch.Stop();
        Console.WriteLine("List.ForEach: {0}", watch.Elapsed);
      }
    }
    
    void Prepare()
    {
      int result = 0;
      foreach (var x in new List<int>(Enumerable.Range(1, 1000)))
      {
        result += x;
      }
      
      result = 0;
      new List<int>(Enumerable.Range(1, 1000)).ForEach(x => result += x);
    }
    
    int Sum_foreach(List<int> theList)
    {
      int result = 0;
      foreach (var x in theList)
      {
        result += x;
      }
      return result;
    }
    
    int Sum_List_ForEach(List<int> theList)
    {
      int result = 0;
      theList.ForEach(x => result += x);
      return result;
    }
  }
  #endregion


で、実行すると私の環境では大体以下のような結果になりました。

  ===== [Count:1000] =====
  foreach:      00:00:00.0004198
  List.ForEach: 00:00:00.0005319
  ===== [Count:3000] =====
  foreach:      00:00:00.0000530
  List.ForEach: 00:00:00.0000391
  ===== [Count:5000] =====
  foreach:      00:00:00.0000821
  List.ForEach: 00:00:00.0000600
  ===== [Count:10000] =====
  foreach:      00:00:00.0001570
  List.ForEach: 00:00:00.0001150
  ===== [Count:50000] =====
  foreach:      00:00:00.0007730
  List.ForEach: 00:00:00.0005422
  ===== [Count:100000] =====
  foreach:      00:00:00.0015239
  List.ForEach: 00:00:00.0011141
  ===== [Count:150000] =====
  foreach:      00:00:00.0022650
  List.ForEach: 00:00:00.0016231
  ===== [Count:500000] =====
  foreach:      00:00:00.0077358
  List.ForEach: 00:00:00.0054828
  ===== [Count:700000] =====
  foreach:      00:00:00.0107820
  List.ForEach: 00:00:00.0077291
  ===== [Count:1000000] =====
  foreach:      00:00:00.0155810
  List.ForEach: 00:00:00.0111550


確かにList.ForEachの方が速いです。でもなんで??
上記の記事には、理由までは書いていなかったので、ちょっと情報収集してみたところ
以下のリソースを発見。


ILレベルで見ると違いが分かるみたいです。
てことで、先ほどのソースをILDasmで見てみました。


[foreachのIL]

.method private hidebysig instance int32
        Sum_foreach(class [mscorlib]System.Collections.Generic.List`1 theList) cil managed
{
  // コード サイズ       64 (0x40)
  .maxstack  2
  .locals init (int32 V_0,
           int32 V_1,
           int32 V_2,
           valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator V_3,
           bool V_4)
  IL_0000:  nop
  IL_0001:  ldc.i4.0
  IL_0002:  stloc.0
  IL_0003:  nop
  IL_0004:  ldarg.1
  IL_0005:  callvirt   instance valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator class [mscorlib]System.Collections.Generic.List`1::GetEnumerator()
  IL_000a:  stloc.3
  .try
  {
    IL_000b:  br.s       IL_001b
    IL_000d:  ldloca.s   V_3
    IL_000f:  call       instance !0 valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator::get_Current()
    IL_0014:  stloc.1
    IL_0015:  nop
    IL_0016:  ldloc.0
    IL_0017:  ldloc.1
    IL_0018:  add
    IL_0019:  stloc.0
    IL_001a:  nop
    IL_001b:  ldloca.s   V_3
    IL_001d:  call       instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator::MoveNext()
    IL_0022:  stloc.s    V_4
    IL_0024:  ldloc.s    V_4
    IL_0026:  brtrue.s   IL_000d
    IL_0028:  leave.s    IL_0039
  }  // end .try
  finally
  {
    IL_002a:  ldloca.s   V_3
    IL_002c:  constrained. valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator
    IL_0032:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()
    IL_0037:  nop
    IL_0038:  endfinally
  }  // end handler
  IL_0039:  nop
  IL_003a:  ldloc.0
  IL_003b:  stloc.2
  IL_003c:  br.s       IL_003e
  IL_003e:  ldloc.2
  IL_003f:  ret
} // end of method ListForEachDiffSamples01::Sum_foreach


IL_000f, IL_001dの部分で2つのcall、つまり、CurrentとMoveNextが呼び出されています。


対して、List.ForEachの方は
[List.ForEachのIL]

.method private hidebysig instance int32
        Sum_List_ForEach(class [mscorlib]System.Collections.Generic.List`1 theList) cil managed
{
  // コード サイズ       44 (0x2c)
  .maxstack  3
  .locals init (class Gsf.Samples.ListForEachDiffSamples01/'<>c__DisplayClass4' V_0,
           int32 V_1)
  IL_0000:  newobj     instance void Gsf.Samples.ListForEachDiffSamples01/'<>c__DisplayClass4'::.ctor()
  IL_0005:  stloc.0
  IL_0006:  nop
  IL_0007:  ldloc.0
  IL_0008:  ldc.i4.0
  IL_0009:  stfld      int32 Gsf.Samples.ListForEachDiffSamples01/'<>c__DisplayClass4'::result
  IL_000e:  ldarg.1
  IL_000f:  ldloc.0
  IL_0010:  ldftn      instance void Gsf.Samples.ListForEachDiffSamples01/'<>c__DisplayClass4'::'b__3'(int32)
  IL_0016:  newobj     instance void class [mscorlib]System.Action`1::.ctor(object,
                                                                                   native int)
  IL_001b:  callvirt   instance void class [mscorlib]System.Collections.Generic.List`1::ForEach(class [mscorlib]System.Action`1)
  IL_0020:  nop
  IL_0021:  ldloc.0
  IL_0022:  ldfld      int32 Gsf.Samples.ListForEachDiffSamples01/'<>c__DisplayClass4'::result
  IL_0027:  stloc.1
  IL_0028:  br.s       IL_002a
  IL_002a:  ldloc.1
  IL_002b:  ret
} // end of method ListForEachDiffSamples01::Sum_List_ForEach


List.ForEachの場合、IL_001bのcallvirt一発のみです。
これが速度の差になっていたって事ですね。
まあ、結局差は微々たるものなので、どっち使っても構わないと思います。


上記リソースの記事には、もっと詳しく計測されていてグラフもありますので
凄くわかりやすいです。勉強になりました。m(_ _)m



================================
過去の記事については、以下のページからご参照下さい。

サンプルコードは、以下の場所で公開しています。