Разница в производительности для структур управления 'for' и 'foreach' в C # - PullRequest
103 голосов
/ 14 июля 2009

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

1

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

2

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

Ответы [ 9 ]

130 голосов
/ 14 июля 2009

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

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

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

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

РЕДАКТИРОВАТЬ: Для тех из вас, кто заявляет, что итерация по 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

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

14 голосов
/ 14 июля 2009

Цикл 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 даже не имеет индексатора, поэтому вы не сможете выполнить цикл for в связанном списке; но если бы вы могли, индексатор должен был бы быть написан так:

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

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

12 голосов
/ 14 июля 2009

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

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 - он был на 20 миллисекунд быстрее - последовательно . Значение for составляло 135-139 мс, а значение foreach - 113-119 мс. Я несколько раз обменивался взад-вперед, убедившись, что это не какой-то процесс, который только что включился.

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

int foo = intList[i];

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

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

edit: вот новый код, использующий предложения Jons (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: 2005мс

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

9 голосов
/ 14 июля 2009

Примечание: этот ответ относится больше к Java, чем к C #, поскольку C # не имеет индексатора для LinkedLists, но я думаю, что общая точка зрения остается в силе.

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

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

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

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

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, а массив, поведение совершенно другое.

2 голосов
/ 14 июля 2009

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

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

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

1 голос
/ 03 октября 2013

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

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

РЕДАКТИРОВАТЬ: Есть коллекции в C # (например, HashSet), которые не имеют индексатора. В этих коллекциях foreach является единственным способом итерации, и это единственный случай, я думаю, его следует использовать более для .

0 голосов
/ 24 сентября 2018

Вы можете прочитать об этом в Deep .NET - часть 1 Итерация

это покрытие результатов (без первой инициализации) из исходного кода .NET вплоть до разборки.

например - Итерация массива с циклом foreach: enter image description here

и - итерация списка с циклом foreach: enter image description here

и конечные результаты: enter image description here

enter image description here

0 голосов
/ 17 февраля 2014

В приведенном вами примере определенно лучше использовать цикл 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               ║
╚══════════════════════╩═══════════╩═══════╩════════════════════════╩═════════════════════╝

0 голосов
/ 29 августа 2013

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...