Перечисление через интерфейс - потеря производительности
У меня был небольшой спор (который был очень близок к священной войне:)) с моим коллегой, о производительности доступа к списку через indeces VS через перечислитель. Чтобы работать с некоторыми фактами, я написал следующий тест:
static void Main(string[] args)
{
const int count = 10000000;
var stopwatch = new Stopwatch();
var list = new List<int>(count);
var rnd = new Random();
for (int i = 0; i < count; i++)
{
list.Add( rnd.Next());
}
const int repeat = 20;
double indeces = 0;
double forEach = 0;
for (int iteration = 0; iteration < repeat; iteration++)
{
stopwatch.Restart();
long tmp = 0;
for (int i = 0; i < count; i++)
{
tmp += list[i];
}
indeces += stopwatch.Elapsed.TotalSeconds;
stopwatch.Restart();
foreach (var integer in list)
{
tmp += integer;
}
forEach += stopwatch.Elapsed.TotalSeconds;
}
Console.WriteLine(indeces /repeat);
Console.WriteLine(forEach /repeat);
}
Фактически, он просто обращается к элементам.
Как я и ожидал, доступ к индексу был быстрее. Это результаты сборки Release на моей машине:
0.0347//index access
0.0737//enumerating
Однако я решил немного изменить тест:
//the same as before
...
IEnumerable<int> listAsEnumerable = list;
//the same as before
...
foreach (var integer in listAsEnumerable)
{
tmp += integer;
}
...
И теперь вышло следующее:
0.0321//index access
0.1246//enumerating (2x slower!)
Если мы перечисляем один и тот же список через интерфейс, производительность 2 раза медленнее!
Почему это происходит?
this означает "перечисление через интерфейс в 2 раза медленнее, чем перечисление фактического списка".
Моя догадка заключается в том, что среда выполнения использует разные Enumerator
s: список в первом тесте и общий во втором тесте.
Ответы
Ответ 1
При использовании List<T>
foreach
фактически не использует интерфейс IEnumerable<T>
; скорее, он использует List<T>.Enumerator
, который является struct
. На тривиальном уровне это означает немного меньшую косвенность - не нужно отменять ссылки и использовать статические вызовы, а не виртуальные вызовы - и более прямую реализацию.
Эти различия очень малы, и в любом разумном реальном примере разница - это шум. Однако это может быть незначительно заметно, если тестирование только производительности foreach
.
Чтобы развернуть это: foreach
фактически не требует IEnumerable[<T>]
- он может работать чисто по шаблону GetEnumerator()
/.MoveNext()
/.Current
/.Dispose()
; это было особенно важно до дженериков в 2.0.
Однако это возможно только тогда, когда переменная вводится как List<T>
(у которой есть собственный метод GetEnumerator()
). Если у вас есть IEnumerable<T>
, он должен использовать IEnumerator<T>
Ответ 2
Здесь вы можете увидеть код:
static void Main()
{
List<int> list = new List<int>(Enumerable.Range(1,10000));
int total = 0;
foreach (var i in list)
{
total += i;
}
IEnumerable<int> enumerable = list;
foreach (var i in enumerable)
{
total += i;
}
Console.ReadLine();
}
Что генерирует этот ИЛ. Обратите внимание на разницу между
System.Collections.Generic.List`1/Enumerator<int32>
и
System.Collections.Generic.IEnumerable`1<int32>
и обратите внимание, что это ValueType
(struct):
.method private hidebysig static void Main() cil managed
{
.entrypoint
// Code size 146 (0x92)
.maxstack 2
.locals init ([0] class [mscorlib]System.Collections.Generic.List`1<int32> list,
[1] int32 total,
[2] int32 i,
[3] class [mscorlib]System.Collections.Generic.IEnumerable`1<int32> enumerable,
[4] valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32> CS$5$0000,
[5] bool CS$4$0001,
[6] class [mscorlib]System.Collections.Generic.IEnumerator`1<int32> CS$5$0002)
IL_0000: nop
IL_0001: ldc.i4.1
IL_0002: ldc.i4 0x2710
IL_0007: call class [mscorlib]System.Collections.Generic.IEnumerable`1<int32> [System.Core]System.Linq.Enumerable::Range(int32,
int32)
IL_000c: newobj instance void class [mscorlib]System.Collections.Generic.List`1<int32>::.ctor(class [mscorlib]System.Collections.Generic.IEnumerable`1<!0>)
IL_0011: stloc.0
IL_0012: ldc.i4.0
IL_0013: stloc.1
IL_0014: nop
IL_0015: ldloc.0
IL_0016: callvirt instance valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<!0> class [mscorlib]System.Collections.Generic.List`1<int32>::GetEnumerator()
IL_001b: stloc.s CS$5$0000
.try
{
IL_001d: br.s IL_002d
IL_001f: ldloca.s CS$5$0000
IL_0021: call instance !0 valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::get_Current()
IL_0026: stloc.2
IL_0027: nop
IL_0028: ldloc.1
IL_0029: ldloc.2
IL_002a: add
IL_002b: stloc.1
IL_002c: nop
IL_002d: ldloca.s CS$5$0000
IL_002f: call instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::MoveNext()
IL_0034: stloc.s CS$4$0001
IL_0036: ldloc.s CS$4$0001
IL_0038: brtrue.s IL_001f
IL_003a: leave.s IL_004b
} // end .try
finally
{
IL_003c: ldloca.s CS$5$0000
IL_003e: constrained. valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>
IL_0044: callvirt instance void [mscorlib]System.IDisposable::Dispose()
IL_0049: nop
IL_004a: endfinally
} // end handler
IL_004b: nop
IL_004c: ldloc.0
IL_004d: stloc.3
IL_004e: nop
IL_004f: ldloc.3
IL_0050: callvirt instance class [mscorlib]System.Collections.Generic.IEnumerator`1<!0> class [mscorlib]System.Collections.Generic.IEnumerable`1<int32>::GetEnumerator()
IL_0055: stloc.s CS$5$0002
.try
{
IL_0057: br.s IL_0067
IL_0059: ldloc.s CS$5$0002
IL_005b: callvirt instance !0 class [mscorlib]System.Collections.Generic.IEnumerator`1<int32>::get_Current()
IL_0060: stloc.2
IL_0061: nop
IL_0062: ldloc.1
IL_0063: ldloc.2
IL_0064: add
IL_0065: stloc.1
IL_0066: nop
IL_0067: ldloc.s CS$5$0002
IL_0069: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
IL_006e: stloc.s CS$4$0001
IL_0070: ldloc.s CS$4$0001
IL_0072: brtrue.s IL_0059
IL_0074: leave.s IL_008a
} // end .try
finally
{
IL_0076: ldloc.s CS$5$0002
IL_0078: ldnull
IL_0079: ceq
IL_007b: stloc.s CS$4$0001
IL_007d: ldloc.s CS$4$0001
IL_007f: brtrue.s IL_0089
IL_0081: ldloc.s CS$5$0002
IL_0083: callvirt instance void [mscorlib]System.IDisposable::Dispose()
IL_0088: nop
IL_0089: endfinally
} // end handler
IL_008a: nop
IL_008b: call string [mscorlib]System.Console::ReadLine()
IL_0090: pop
IL_0091: ret
} // end of method Program2::Main
Ответ 3
Если вы посмотрите на IL для обеих версий, вы увидите, что первая версия использует итератор типа System.Collections.Generic.List<System.Int32>+Enumerator
- вложенный struct
, который оптимизирован для итерации по списку.
Вторая версия использует общую реализацию System.Collections.Generic.IEnumerator<System.Int32>
, которая менее эффективна, потому что она не "обманывает", сохраняя частный индекс в текущем элементе в списке.
Ответ 4
Я подозреваю, что при использовании вместо foreach (по крайней мере, для примитивных типов) есть увеличение производительности. Насколько я знаю, они почти эквивалентны, если вы выполняете for и foreach по одному и тому же массиву (не любая другая структура, такая как списки, это создает некоторые накладные расходы отдельно).
Производительность foreach и for зависит от того, какой тип структуры вы используете для foreach.
Пожалуйста, проверьте;
Сравнение For и Foreach