Почему второй цикл for всегда выполняется быстрее первого? - PullRequest
5 голосов
/ 20 июня 2009

Я пытался выяснить, был ли цикл for быстрее цикла foreach и использовал ли классы System.Diagnostics для определения времени выполнения задачи. Во время выполнения теста я заметил, что цикл, который я ставлю первым, всегда выполняется медленнее, чем последний. Может кто-нибудь сказать, пожалуйста, почему это происходит? Мой код ниже:

using System;
using System.Diagnostics;

namespace cool {
    class Program {
        static void Main(string[] args) {
            int[] x = new int[] { 3, 6, 9, 12 };
            int[] y = new int[] { 3, 6, 9, 12 };

            DateTime startTime = DateTime.Now;
            for (int i = 0; i < 4; i++) {
                Console.WriteLine(x[i]);
            }
            TimeSpan elapsedTime = DateTime.Now - startTime;

            DateTime startTime2 = DateTime.Now;
            foreach (var item in y) {
                Console.WriteLine(item);
            }
            TimeSpan elapsedTime2 = DateTime.Now - startTime2;

            Console.WriteLine("\nSummary");
            Console.WriteLine("--------------------------\n");
            Console.WriteLine("for:\t{0}\nforeach:\t{1}", elapsedTime, elapsedTime2);

            Console.ReadKey();
      }
   }
}

Вот вывод:

for:            00:00:00.0175781
foreach:        00:00:00.0009766

Ответы [ 8 ]

16 голосов
/ 20 июня 2009

Возможно, потому что классы (например, Console) должны быть JIT-скомпилированы с первого раза. Вы получите лучшие показатели, сначала вызвав все методы (для их JIT (прогрев, затем прогрев)), а затем выполнив тест.

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

Кстати, разница в производительности между for и foreach будет незначительной, а преимущества удобочитаемости при использовании foreach почти всегда перевешивают любое предельное преимущество в производительности.

7 голосов
/ 20 июня 2009
  1. Я бы не использовал DateTime для измерения производительности - попробуйте класс Stopwatch.
  2. Измерение всего за 4 прохода никогда не даст вам хорошего результата. Лучше использовать> 100.000 проходов (вы можете использовать внешний цикл). Не делайте Console.WriteLine в вашем цикле.
  3. Еще лучше: используйте профилировщик (например, Redgate ANTS или, возможно, NProf)
3 голосов
/ 20 июня 2009

Я просто проводил тесты, чтобы получить реальные цифры, но в то же время Гэс побил меня до ответа - звонил в Console.Writeline соединяется при первом звонке, поэтому вы оплачиваете эту стоимость в первом цикле. *

Только для информации - используя секундомер вместо даты и времени и измеряя количество тактов:

Без вызова Console.Writeline перед первым циклом время было

for: 16802
foreach: 2282

с вызовом на Console.Writeline они были

for: 2729
foreach: 2268

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


Отредактированный код для справки:

        int[] x = new int[] { 3, 6, 9, 12 };
        int[] y = new int[] { 3, 6, 9, 12 };

        Console.WriteLine("Hello World");

        Stopwatch sw = new Stopwatch();

        sw.Start();
        for (int i = 0; i < 4; i++)
        {
            Console.WriteLine(x[i]);
        }
        sw.Stop();
        long elapsedTime = sw.ElapsedTicks;

        sw.Reset();
        sw.Start();
        foreach (var item in y)
        {
            Console.WriteLine(item);
        }
        sw.Stop();
        long elapsedTime2 = sw.ElapsedTicks;

        Console.WriteLine("\nSummary");
        Console.WriteLine("--------------------------\n");
        Console.WriteLine("for:\t{0}\nforeach:\t{1}", elapsedTime, elapsedTime2);

        Console.ReadKey();
3 голосов
/ 20 июня 2009

Я не так много в C #, но когда я правильно помню, Microsoft создавала компиляторы "Just in Time" для Java.Когда они используют одинаковые или похожие методы в C #, вполне естественно, что «некоторые конструкции, прибывающие вторыми, работают быстрее».

Например, может случиться так, что JIT-система увидит, что цикл выполняется ирешает adhoc для компиляции всего метода.Следовательно, когда достигается второй цикл, он все же компилируется и работает намного быстрее, чем первый.Но это довольно упрощенное предположение, мое.Конечно, вам нужно гораздо более глубокое понимание системы времени выполнения C #, чтобы понять, что происходит.Также может случиться так, что к RAM-странице обращаются первыми в первом цикле, а во втором она все еще находится в кэше ЦП.

Аддон: другой сделанный комментарий:Модуль вывода может быть JIT в первый раз в швах первого цикла для меня более вероятно, чем моя первая догадка.Современные языки просто очень сложны, чтобы выяснить, что делается под капотом.Также мое утверждение вписывается в это предположение:

Но у вас также есть терминальные выходы в ваших циклах.Они делают вещи еще сложнее.Может также случиться так, что первый раз в программе стоит открыть терминал в программе.

2 голосов
/ 20 июня 2009

Причина, по которой существует несколько видов служебных данных в версии foreach, которых нет в цикле for

  • Использование IDisposable.
  • Дополнительный вызов метода для каждого элемента. Каждый элемент должен быть доступен изнутри с помощью IEnumerator<T>.Current, который является вызовом метода. Поскольку он находится на интерфейсе, он не может быть встроен. Это означает, что N вызывает метод, где N - количество элементов в перечислении. Цикл for просто использует и индексатор
  • В цикле foreach все вызовы проходят через интерфейс. В целом это немного медленнее, чем через конкретный тип

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

Также отметим, что, как отметил Мерад, компиляторы и JIT могут выбрать оптимизацию цикла foreach для определенных известных структур данных, таких как массив. Конечным результатом может быть просто цикл for.

Примечание. Для оценки производительности в целом требуется немного больше работы.

  • Вы должны использовать секундомер вместо DateTime. Это гораздо точнее для оценки производительности.
  • Вы должны выполнить тест много раз, а не один раз
  • Вам необходимо выполнить фиктивный прогон в каждом цикле, чтобы устранить проблемы, возникающие при первом использовании метода JIT. Это, вероятно, не проблема, когда весь код находится в одном методе, но это не повредит.
  • Вам нужно использовать более 4 значений в списке. Попробуйте 40000 вместо этого.
1 голос
/ 20 июня 2009

Я бы не стал читать слишком много - это не очень хороший код для профилирования по следующим причинам
1. DateTime не предназначен для профилирования. Вы должны использовать QueryPerformanceCounter или StopWatch, которые используют счетчики аппаратного профиля процессора
2. Console.WriteLine - это метод устройства, поэтому могут быть тонкие эффекты, такие как буферизация, для учета
3. Выполнение одной итерации каждого блока кода никогда не даст вам точных результатов, потому что ваш ЦП выполняет много лишних оптимизаций на лету, таких как выполнение вне порядка и планирование команд
4. Скорее всего, код, который получает JITed для обоих кодовых блоков, довольно похож, поэтому, скорее всего, он будет в кеше команд для второго кодового блока

Чтобы получить лучшее представление о сроках, я сделал следующее

  1. Заменил Console.WriteLine математическим выражением (e ^ num)
  2. Я использовал QueryPerformanceCounter / QueryPerformanceTimer через P / Invoke
  3. Я запускал каждый кодовый блок 1 миллион раз, затем усреднял результаты

Когда я это сделал, я получил следующие результаты:

Цикл for занял 0,000676 миллисекунд
Цикл foreach занял 0,000653 миллисекунды

Итак, foreach был немного быстрее, но ненамного

Затем я провел еще несколько экспериментов и сначала запустил блок foreach, а затем блок for
. Когда я это сделал, я получил следующие результаты:

Цикл foreach занял 0,000702 миллисекунды
Цикл for занял 0,000691 миллисекунд

Наконец я дважды запустил оба цикла, т. Е. Для + foreach, затем для + foreach снова
Когда я это сделал, я получил следующие результаты:

Цикл foreach занял 0,00140 миллисекунд
Цикл for занял 0,001385 миллисекунд

Так что в основном мне кажется, что какой бы код вы ни выполняли вторым, он работает немного быстрее достаточно, чтобы иметь какое-либо значение.
--Edit -
Вот пара полезных ссылок
Как синхронизировать код с помощью QueryPerformanceCounter
Кеш инструкций
Вне исполнения заказа

1 голос
/ 20 июня 2009

Я не понимаю, почему все здесь говорят, что for будет быстрее, чем foreach в данном конкретном случае. Для List<T> это (примерно в 2 раза медленнее foreach через Список, чем for через List<T>).

Фактически, foreach будет немного быстрее , чем for здесь. Потому что foreach в массиве по существу компилируется в:

for(int i = 0; i < array.Length; i++) { }

Использование .Length в качестве критерия остановки позволяет JIT удалить проверки границ при доступе к массиву, поскольку это особый случай. Использование i < 4 заставляет JIT вставлять дополнительные инструкции, чтобы проверять каждую итерацию, находится ли i за пределами массива, и генерировать исключение, если это так. Однако с .Length это может гарантировать, что вы никогда не выйдете за границы массива, поэтому проверки границ являются избыточными, что делает его быстрее.

Однако в большинстве циклов накладные расходы цикла незначительны по сравнению с работой внутри.

Расхождение, которое вы видите, может быть объяснено только JIT, я думаю.

1 голос
/ 20 июня 2009

Вы должны использовать секундомер для определения времени поведения.

Технически цикл для работает быстрее. Foreach вызывает метод MoveNext () (создавая стек метода и другие накладные расходы из вызова) на итераторе IEnumerable, когда для должен только увеличивать переменную.

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