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`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
================================
過去の記事については、以下のページからご参照下さい。
サンプルコードは、以下の場所で公開しています。