List
ちょっと自分でも調べましたのでメモメモ。
元々、CodeRush Tips & Tricksの以下の記事を見たのがきっかけでした。
- The Introduce ForEach Action refactoring
上記記事の中に、「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の方が速いです。でもなんで??
上記の記事には、理由までは書いていなかったので、ちょっと情報収集してみたところ
以下のリソースを発見。
- Why is List
.ForEach faster than standard foreach? - Performance of foreach vs. List.ForEach
ILレベルで見ると違いが分かるみたいです。
てことで、先ほどのソースをILDasmで見てみました。
[foreachのIL]
.method private hidebysig instance int32 Sum_foreach(class [mscorlib]System.Collections.Generic.List`1theList) 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`1theList) 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
================================
過去の記事については、以下のページからご参照下さい。
- いろいろ備忘録日記まとめ
サンプルコードは、以下の場所で公開しています。
- いろいろ備忘録日記サンプルソース置き場