Разница в производительности для структур управления "for" и "foreach" в С#

Какой фрагмент кода даст лучшую производительность? Ниже сегменты кода были написаны на С#.

1.

for(int counter=0; counter<list.Count; counter++)
{
    list[counter].DoSomething();
}

2.

foreach(MyType current in list)
{
    current.DoSomething();
}

Ответ 1

Ну, это отчасти зависит от точного типа list. Он также будет зависеть от того, какой именно CLR вы используете.

Значит ли это каким-либо значимым или не будет, зависит от того, выполняете ли вы какую-либо реальную работу в цикле. Почти во всех случаях разница в производительности не будет существенной, но разница в читаемости благоприятствует циклу foreach.

Я лично использовал LINQ, чтобы избежать "if" тоже:

foreach (var item in list.Where(condition))
{
}

EDIT: для тех из вас, кто утверждает, что итерация по List<T> с помощью foreach создает тот же код, что и цикл for, здесь видно, что это не так:

static void IterateOverList(List<object> list)
{
    foreach (object o in list)
    {
        Console.WriteLine(o);
    }
}

Производит IL:

.method private hidebysig static void  IterateOverList(class [mscorlib]System.Collections.Generic.List`1<object> list) cil managed
{
  // Code size       49 (0x31)
  .maxstack  1
  .locals init (object V_0,
           valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<object> V_1)
  IL_0000:  ldarg.0
  IL_0001:  callvirt   instance valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<!0> class [mscorlib]System.Collections.Generic.List`1<object>::GetEnumerator()
  IL_0006:  stloc.1
  .try
  {
    IL_0007:  br.s       IL_0017
    IL_0009:  ldloca.s   V_1
    IL_000b:  call       instance !0 valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<object>::get_Current()
    IL_0010:  stloc.0
    IL_0011:  ldloc.0
    IL_0012:  call       void [mscorlib]System.Console::WriteLine(object)
    IL_0017:  ldloca.s   V_1
    IL_0019:  call       instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<object>::MoveNext()
    IL_001e:  brtrue.s   IL_0009
    IL_0020:  leave.s    IL_0030
  }  // end .try
  finally
  {
    IL_0022:  ldloca.s   V_1
    IL_0024:  constrained. valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<object>
    IL_002a:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()
    IL_002f:  endfinally
  }  // end handler
  IL_0030:  ret
} // end of method Test::IterateOverList

Компилятор обрабатывает массивы по-разному, преобразовывая цикл foreach в основном в цикл for, но не List<T>. Здесь эквивалентный код для массива:

static void IterateOverArray(object[] array)
{
    foreach (object o in array)
    {
        Console.WriteLine(o);
    }
}

// Compiles into...

.method private hidebysig static void  IterateOverArray(object[] 'array') cil managed
{
  // Code size       27 (0x1b)
  .maxstack  2
  .locals init (object V_0,
           object[] V_1,
           int32 V_2)
  IL_0000:  ldarg.0
  IL_0001:  stloc.1
  IL_0002:  ldc.i4.0
  IL_0003:  stloc.2
  IL_0004:  br.s       IL_0014
  IL_0006:  ldloc.1
  IL_0007:  ldloc.2
  IL_0008:  ldelem.ref
  IL_0009:  stloc.0
  IL_000a:  ldloc.0
  IL_000b:  call       void [mscorlib]System.Console::WriteLine(object)
  IL_0010:  ldloc.2
  IL_0011:  ldc.i4.1
  IL_0012:  add
  IL_0013:  stloc.2
  IL_0014:  ldloc.2
  IL_0015:  ldloc.1
  IL_0016:  ldlen
  IL_0017:  conv.i4
  IL_0018:  blt.s      IL_0006
  IL_001a:  ret
} // end of method Test::IterateOverArray

Интересно, что я не могу найти эту документацию в спецификации С# 3 где угодно...

Ответ 2

Цикл

A for скомпилирован для кода, приблизительно эквивалентного этому:

int tempCount = 0;
while (tempCount < list.Count)
{
    if (list[tempCount].value == value)
    {
        // Do something
    }
    tempCount++;
}

Где в цикле foreach скомпилируется код, приблизительно эквивалентный этому:

using (IEnumerator<T> e = list.GetEnumerator())
{
    while (e.MoveNext())
    {
        T o = (MyClass)e.Current;
        if (row.value == value)
        {
            // Do something
        }
    }
}

Итак, как вы можете видеть, все будет зависеть от того, как выполняется перечислитель, и как реализуется индексатор списков. Как оказалось, перечислитель для типов на основе массивов обычно записывает что-то вроде этого:

private static IEnumerable<T> MyEnum(List<T> list)
{
    for (int i = 0; i < list.Count; i++)
    {
        yield return list[i];
    }
}

Итак, как вы можете видеть, в этом случае это не будет иметь особого значения, однако перечислитель для связанного списка, вероятно, будет выглядеть примерно так:

private static IEnumerable<T> MyEnum(LinkedList<T> list)
{
    LinkedListNode<T> current = list.First;
    do
    {
        yield return current.Value;
        current = current.Next;
    }
    while (current != null);
}

В .NET вы обнаружите, что класс LinkedList <T> даже не имеет индексатора, поэтому вы не сможете сделать свой цикл for связанным списком; но если бы вы могли, индексатор должен был бы быть написан так:

public T this[int index]
{
       LinkedListNode<T> current = this.First;
       for (int i = 1; i <= index; i++)
       {
            current = current.Next;
       }
       return current.value;
}

Как вы можете видеть, вызов этого многократного цикла в цикле будет намного медленнее, чем использование перечислителя, который может запомнить, где он находится в списке.

Ответ 3

Легкий тест для полуавтоматизации. Я сделал небольшой тест, просто чтобы посмотреть. Вот код:

static void Main(string[] args)
{
    List<int> intList = new List<int>();

    for (int i = 0; i < 10000000; i++)
    {
        intList.Add(i);
    }

    DateTime timeStarted = DateTime.Now;
    for (int i = 0; i < intList.Count; i++)
    {
        int foo = intList[i] * 2;
        if (foo % 2 == 0)
        {
        }
    }

    TimeSpan finished = DateTime.Now - timeStarted;

    Console.WriteLine(finished.TotalMilliseconds.ToString());
    Console.Read();

}

И вот раздел foreach:

foreach (int i in intList)
{
    int foo = i * 2;
    if (foo % 2 == 0)
    {
    }
}

Когда я заменил for foreach - foreach был на 20 миллисекунд быстрее - последовательно. Это было 135-139 мс, в то время как foreach составлял 113-119 мс. Я несколько раз менял местами взад-вперед, убедившись, что это не какой-то процесс, который только что начал.

Однако, когда я удалил оператор foo и if, значение for было быстрее на 30 мс (foreach составлял 88 мс, а для 59 мс). Они были пустыми раковинами. Я предполагаю, что foreach фактически передал переменную, где for for просто увеличивал переменную. Если бы я добавил

int foo = intList[i];

Затем за медленнее около 30 мс. Я предполагаю, что это связано с этим, создавая foo и захватывая переменную в массиве и присваивая ее foo. Если вы просто обращаетесь к intList [i], то у вас нет этого штрафа.

Честно говоря, я ожидал, что foreach будет немного медленнее при любых обстоятельствах, но этого недостаточно для большинства приложений.

edit: вот новый код с помощью предложений Джонса (134217728 - это самый большой из них, который вы можете получить до того, как будет вызвано исключение System.OutOfMemory):

static void Main(string[] args)
{
    List<int> intList = new List<int>();

    Console.WriteLine("Generating data.");
    for (int i = 0; i < 134217728 ; i++)
    {
        intList.Add(i);
    }

    Console.Write("Calculating for loop:\t\t");

    Stopwatch time = new Stopwatch();
    time.Start();
    for (int i = 0; i < intList.Count; i++)
    {
        int foo = intList[i] * 2;
        if (foo % 2 == 0)
        {
        }
    }

    time.Stop();
    Console.WriteLine(time.ElapsedMilliseconds.ToString() + "ms");
    Console.Write("Calculating foreach loop:\t");
    time.Reset();
    time.Start();

    foreach (int i in intList)
    {
        int foo = i * 2;
        if (foo % 2 == 0)
        {
        }
    }

    time.Stop();

    Console.WriteLine(time.ElapsedMilliseconds.ToString() + "ms");
    Console.Read();
}

И вот результаты:

Генерация данных. Расчет для цикла: 2458 мс Вычисление цикла foreach: 2005ms

Сворачивание их вокруг, чтобы увидеть, имеет ли он дело с порядком вещей, дает одинаковые результаты (почти).

Ответ 4

Примечание. Этот ответ больше применим к Java, чем к С#, поскольку на С# нет индексатора на LinkedLists, но я думаю, что общая точка по-прежнему сохраняется.

Если list, с которым вы работаете, является LinkedList, производительность индексатора (доступ к массиву) намного хуже, чем использование IEnumerator из foreach, для больших списков.

Когда вы получаете доступ к элементу 10.000 в LinkedList с использованием синтаксиса индексатора: list[10000], связанный список будет начинаться в начале node и пересекать Next -поток десять тысяч раз, пока он не достигнет правильный объект. Очевидно, что если вы сделаете это в цикле, вы получите:

list[0]; // head
list[1]; // head.Next
list[2]; // head.Next.Next
// etc.

Когда вы вызываете GetEnumerator (неявно используя forach -syntax), вы получите объект IEnumerator, который имеет указатель на голову node. Каждый раз, когда вы вызываете MoveNext, этот указатель перемещается в следующий node, например:

IEnumerator em = list.GetEnumerator();  // Current points at head
em.MoveNext(); // Update Current to .Next
em.MoveNext(); // Update Current to .Next
em.MoveNext(); // Update Current to .Next
// etc.

Как вы можете видеть, в случае LinkedList s метод индексации массива становится медленнее и медленнее, чем дольше вы выполняете цикл (он должен проходить один и тот же указатель главы снова и снова). В то время как IEnumerable работает только в постоянное время.

Конечно, как сказал Джон, это действительно зависит от типа list, если list не является LinkedList, а массивом, поведение совершенно другое.

Ответ 5

Как и другие люди, упомянутые, хотя производительность на самом деле не имеет большого значения, foreach всегда будет немного медленнее из-за использования IEnumerable/IEnumerator в цикле. Компилятор переводит конструкцию в вызовы на этом интерфейсе, и для каждого шага в конструкции foreach вызывается функция + свойство.

IEnumerator iterator = ((IEnumerable)list).GetEnumerator();
while (iterator.MoveNext()) {
  var item = iterator.Current;
  // do stuff
}

Это эквивалентное разложение конструкции в С#. Вы можете себе представить, как влияние производительности может варьироваться в зависимости от реализаций MoveNext и Current. Если в доступе к массиву у вас нет этих зависимостей.

Ответ 6

После прочтения достаточно аргументов, что "цикл foreach должен быть предпочтительным для удобочитаемости", могу сказать, что моя первая реакция была "что"? Считываемость, в общем, субъективна и, в данном случае, еще больше. Для тех, у кого есть опыт программирования (практически, каждый язык перед Java), для циклов гораздо легче читать, чем foreach. Кроме того, те же люди, утверждающие, что петли foreach более читабельны, также являются сторонниками linq и других "функций", которые делают код трудным для чтения и обслуживания, что доказывает вышеприведенную точку.

О влиянии на производительность см. ответ на этот вопрос.

EDIT: Есть коллекции в С# (например, HashSet), у которых нет индексатора. В этих коллекциях foreach - единственный способ повторения, и это единственный случай, который, по моему мнению, должен использоваться.

Ответ 7

Есть еще один интересный факт, который можно легко пропустить при тестировании скорости обеих петель: Использование режима отладки не позволяет компилятору оптимизировать код с использованием настроек по умолчанию.

Это привело меня к интересному результату, когда foreach работает быстрее, чем в режиме отладки. В то время как for ist быстрее, чем foreach в режиме выпуска. Очевидно, что у компилятора есть лучшие способы оптимизации цикла for, чем цикл foreach, который компрометирует несколько вызовов методов. Цикл for, кстати, настолько фундаментален, что возможно, что он даже оптимизирован самим процессором.

Ответ 8

В примере, который вы указали, определенно лучше использовать цикл foreach вместо цикла for.

Стандартная конструкция foreach может быть более быстрой (1,5 цикла на шаг), чем простая for-loop (2 цикла на шаг), если цикл не развернут (1.0 циклов на шаг).

Таким образом, для повседневного кода производительность не является основанием для использования более сложных конструкций for, while или do-while.

Посмотрите эту ссылку: http://www.codeproject.com/Articles/146797/Fast-and-Less-Fast-Loops-in-C


╔══════════════════════╦═══════════╦═══════╦════════════════════════╦═════════════════════╗
║        Method        ║ List<int> ║ int[] ║ Ilist<int> onList<Int> ║ Ilist<int> on int[] ║
╠══════════════════════╬═══════════╬═══════╬════════════════════════╬═════════════════════╣
║ Time (ms)            ║ 23,80     ║ 17,56 ║ 92,33                  ║ 86,90               ║
║ Transfer rate (GB/s) ║ 2,82      ║ 3,82  ║ 0,73                   ║ 0,77                ║
║ % Max                ║ 25,2%     ║ 34,1% ║ 6,5%                   ║ 6,9%                ║
║ Cycles / read        ║ 3,97      ║ 2,93  ║ 15,41                  ║ 14,50               ║
║ Reads / iteration    ║ 16        ║ 16    ║ 16                     ║ 16                  ║
║ Cycles / iteration   ║ 63,5      ║ 46,9  ║ 246,5                  ║ 232,0               ║
╚══════════════════════╩═══════════╩═══════╩════════════════════════╩═════════════════════╝