Разница в производительности для управляющих структур for и foreach в C #

105

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

1.

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

2.

foreach(MyType current in list)
{
    current.DoSomething();
}
Ктевар
источник
31
Я полагаю, что это не имеет значения. Если у вас проблемы с производительностью, это почти наверняка не из-за этого. Не то чтобы тебе не следовало задавать этот вопрос ...
дарасд
2
Если ваше приложение не очень критично по производительности, я бы не стал беспокоиться об этом. Намного лучше иметь чистый и понятный код.
Fortyrunner
2
Меня беспокоит, что некоторые из ответов здесь, кажется, отправлены людьми, у которых просто нет концепции итератора в своем мозгу, и, следовательно, нет концепции счетчиков или указателей.
Эд Джеймс,
3
Этот второй код не компилируется. System.Object не имеет члена с именем 'value' (если вы действительно не злой, определили его как метод расширения и сравниваете делегаты). Сильно введите свой foreach.
Trillian,
1
Первый код также не будет компилироваться, если только тип listдействительно не имеет countчлена вместо Count.
Джон Скит,

Ответы:

130

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

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

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

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

РЕДАКТИРОВАТЬ: Для тех из вас, кто утверждает, что повторение с помощью List<T>with foreachдает тот же код, что и forцикл, вот свидетельство того, что это не так:

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

Производит ИЖ из:

.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 ...

Джон Скит
источник
Из интереса, Джон, сценарий с List <T> выше ... применим ли это и к другим коллекциям? Кроме того, как вы узнали об этом (без какого-либо намерения зла) ... например ... вы буквально наткнулись на это, пытаясь ответить на этот вопрос некоторое время назад? Это так ... случайно / секретно :)
Pure.Krome
5
Некоторое время я знал об оптимизации массивов - массивы - это «основной» вид коллекции; компилятор C # уже хорошо осведомлен о них, поэтому имеет смысл рассматривать их по-другому. У компилятора нет (и не должно быть) каких-либо специальных знаний о List<T>.
Джон Скит,
Ура :) и да ... массивы были первой концепцией коллекций, которой меня учили много лет назад в универе ... так что можно сделать вывод, что компилятор достаточно умен, чтобы иметь дело с одним из (если не самым) самым примитивным типом коллекция. снова ура!
Pure.Krome
3
@JonSkeet Оптимизация итератора списка изменяет поведение при изменении списка во время итерации. Вы теряете исключение, если оно изменено. Оптимизация по-прежнему возможна, но требует проверки, чтобы никаких изменений не происходило (в том числе, я полагаю, в других потоках).
Крейг Гидни,
5
@VeeKeyBee: Так сказала Microsoft в 2004 году. А) все меняется; б) для того, чтобы это было значительным, на каждой итерации необходимо выполнять крошечные объемы работы. Обратите внимание, что foreachнад массивом эквивалентно в forлюбом случае. Всегда сначала кодируйте для удобочитаемости, а затем выполняйте микрооптимизацию только тогда, когда у вас есть доказательства того, что это дает ощутимое преимущество в производительности.
Джон Скит,
15

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;
}

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

Мартин Браун
источник
12

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

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)
    {
    }
}

Когда я заменил на с Еогеаспом - в цикле , была 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: вот новый код с использованием предложений Джонса (134217728 - это самый большой int, который вы можете иметь до того, как будет выбрано исключение 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 мс Расчет для каждого цикла: 2005 мс

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

Кенни Манн
источник
6
Лучше использовать секундомер, чем DateTime.Now - и, честно говоря, я бы не стал доверять такому быстрому запуску.
Джон Скит,
8
Ваши циклы foreach выполняются быстрее, потому что «for» оценивает условие на каждой итерации. В случае вашего примера это вызывает один дополнительный вызов метода (для получения list.count) Короче говоря, вы тестируете два разных фрагмента кода, отсюда и ваши странные результаты. Попробуйте 'int max = intlist.Count; for (int i = 0; i <max; i ++) ... 'и цикл' for 'всегда будет работать быстрее, как и ожидалось!
AR
1
После компиляции for и foreach оптимизируются точно так же при работе с примитивами. Только когда вы введете List <T>, они будут сильно отличаться по скорости.
Энтони Рассел
9

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

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

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

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

Когда вы вызываете GetEnumerator(неявно используя forach-syntax), вы получаете 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.

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

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

Том Локхорст
источник
4
LinkedList в .NET не имеет индексатора, поэтому на самом деле это не вариант.
Джон Скит,
Ну что ж, тогда эта проблема решает :-) Я просто просматриваю LinkedList<T>документацию на MSDN, и у нее довольно приличный API. Самое главное, что у него нет get(int index)метода, как у Java. Тем не менее, я полагаю, что это все еще актуально для любой другой структуры данных в виде списка, которая предоставляет индексатор, который работает медленнее, чем конкретный IEnumerator.
Tom Lokhorst
2

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

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

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

Чарльз Пракаш Дасари
источник
4
Не забывайте, что существует разница между доступом к массиву и доступом к индексатору. Если list находится List<T>здесь, значит, все еще есть хит (возможно, встроенный) вызова индексатора. Это не похоже на доступ к голому металлическому массиву.
Джон Скит,
Совершенно верно! Это еще одна реализация собственности, и мы полностью зависим от ее реализации.
Чарльз Пракаш Дасари
1

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

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

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

ThunderGr
источник
0

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

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

Сэм
источник
0

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

rpax
источник
4
Вы можете перечитать статью проекта кода, которую вы связали. Это интересная статья, но в ней говорится прямо противоположное вашему сообщению. Кроме того, воссозданная вами таблица измеряет производительность прямого доступа к массиву и списку или через их интерфейсы IList. Ни при чем тут вопрос. :)
Пол Уоллс
0

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

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

например - Итерация массива с циклом foreach: введите описание изображения здесь

и - итерация списка с циклом foreach: введите описание изображения здесь

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

введите описание изображения здесь

Или Яков
источник