Thursday, January 17, 2008

C#: 'foreach' or 'for'?

Некоторое время назад у меня с одним моим знакомым разработчиком был разговор, в котором появился вопрос о том, насколько эффективно использовать 'foreach' для итерации по простым массивам данных. Знакомый утверждал, что 'for' эффективней из-за того, что при 'foreach' в любом случае создаётся IEnumerator и вызывается MoveNext.

Решил провести небольшой тест и рассмотреть что же получается при компиляции двух функций:

        private static void Test(string[] test)
{
foreach (string s in test)
{
Console.WriteLine(s);
}
}
private static void Test2(string[] test)
{
for (int c = 0; c < test.Length; c++)
{
Console.WriteLine(test[c]);
}
}

 


В результате мы получаем:

.method private hidebysig static void Test(string[] test) cil managed
{
.maxstack 2
.locals init (
[0] string s,
[1] string[] CS$6$0000,
[2] int32 CS$7$0001)
L_0000: ldarg.0
L_0001: stloc.1
L_0002: ldc.i4.0
L_0003: stloc.2
L_0004: br.s L_0014
L_0006: ldloc.1
L_0007: ldloc.2
L_0008: ldelem.ref
L_0009: stloc.0
L_000a: ldloc.0
L_000b: call void [mscorlib]System.Console::WriteLine(string)
L_0010: ldloc.2
L_0011: ldc.i4.1
L_0012: add
L_0013: stloc.2
L_0014: ldloc.2
L_0015: ldloc.1
L_0016: ldlen
L_0017: conv.i4
L_0018: blt.s L_0006
L_001a: ret
}

.method private hidebysig static void Test2(string[] test) cil managed
{
.maxstack 2
.locals init (
[0] int32 c)
L_0000: ldc.i4.0
L_0001: stloc.0
L_0002: br.s L_0010
L_0004: ldarg.0
L_0005: ldloc.0
L_0006: ldelem.ref
L_0007: call void [mscorlib]System.Console::WriteLine(string)
L_000c: ldloc.0
L_000d: ldc.i4.1
L_000e: add
L_000f: stloc.0
L_0010: ldloc.0
L_0011: ldarg.0
L_0012: ldlen
L_0013: conv.i4
L_0014: blt.s L_0004
L_0016: ret
}

Внимательно проанализировав этот код, можно сделать вывод, что foreach фактически не добавил ничего, кроме объявления двух ссылок - одну для переменной 's', вторую для массива (при этом вторая ссылка на самом деле инициализируется значением ссылки аргумента - т.е. на исходных массив).


Таким образом для массивов использование foreach не несёт никаких дополнительных расходов.


Подчеркну, что это справедливо только для массивов - так как если заменить параметр на IEnumerable<string> (а Resharper "подсказывает" что это имеет смысл сделать), то получаем уже намного более "тяжелый" код:


 

.method private hidebysig static void Test3(class [mscorlib]System.Collections.Generic.IEnumerable`1<string> test) cil managed
{
.maxstack 1
.locals init (
[0] string s,
[1] class [mscorlib]System.Collections.Generic.IEnumerator`1<string> CS$5$0000)
L_0000: ldarg.0
L_0001: callvirt instance class [mscorlib]System.Collections.Generic.IEnumerator`1<!0> [mscorlib]System.Collections.Generic.IEnumerable`1<string>::GetEnumerator()
L_0006: stloc.1
L_0007: br.s L_0016
L_0009: ldloc.1
L_000a: callvirt instance !0 [mscorlib]System.Collections.Generic.IEnumerator`1<string>::get_Current()
L_000f: stloc.0
L_0010: ldloc.0
L_0011: call void [mscorlib]System.Console::WriteLine(string)
L_0016: ldloc.1
L_0017: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
L_001c: brtrue.s L_0009
L_001e: leave.s L_002a
L_0020: ldloc.1
L_0021: brfalse.s L_0029
L_0023: ldloc.1
L_0024: callvirt instance void [mscorlib]System.IDisposable::Dispose()
L_0029: endfinally
L_002a: ret
.try L_0007 to L_0020 finally handler L_0020 to L_002a
}


Вывод: foreach использовать можно, но надо это делать акуратно :)

1 comment:

Дядя Эдик said...

Спасибо. Интересное исследование. В NET разработках я уже и не помню, когда for использовал. Все сплошь foreach.