Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

foreach or for that is the question

Anna | 17.06.2014 | нет комментариев

Вопрос о выборе цикла for/foreach ветх, как мир. Все мы слышали, что foreach работает неторопливей for-а. Но не все знаем почему… А вообще так ли оно?

Когда я начинал постигать .NET, один человек сказал мне, что foreach работает в 2 раза неторопливей for-а, без каких-либо на то обоснований, и я принял это как должное. Сейчас, когда чьих-то слов мне немного, я решил написать эту статью.

В этой статье я исследую продуктивность циклов, а так же уточню некоторые нюансы.

Выходит, поехали!

Разглядим дальнейший код:

foreach (var item in Enumerable.Range(0, 128))
{
  Console.WriteLine(item);
}

Цикл foreach является синтаксическим сахаром и в данном случае компилятор разворачивает его приблизительно в дальнейший код:

IEnumerator<int> enumerator = Enumerable.Range(0, 128).GetEnumerator();
try
 {
   while (enumerator.MoveNext())
   {
     int item = enumerator.Current;
     Console.WriteLine(item);
   }
 }
finally
 {
  if (enumerator != null)
  {
   enumerator.Dispose();
  }
}

Зная это дозволено легко предположить, отчего foreach должен быть неторопливей for-a. При применении foreach-а:

  • создается новейший объект — итератор;
  • на всякой итерации идет вызов способа MoveNext;
  • на всякой итерации идет обращение к свойству Current, что равносильно вызову способа;

Вот и все! Правда нет, не все так просто…

Есть еще один нюанс! К счастью либо, к сожалению C#/CLR имеют кучу оптимизаций. К счастью, потому что код работает стремительней, к сожалению, потому что разработчикам об этом доводится знать (все же я считаю к счастью, причем к огромному).

Скажем, от того что массивы являются типом, крепко интегрированным в CLR, то для них имеется огромное число оптимизаций. Одна из них касается цикла foreach.

Таким образом, значимым аспектом продуктивности цикла foreach является итерируемая сущность, от того что в зависимости от нее он может разворачиваться по-различному.

В статье мы будем рассматривать итерирование массивов и списков. Помимо for-a и foreach-a будем так же рассматривать итерирование с поддержкой статического способа Array.ForEach, а так же экземплярного List.ForEach.

Способы тестирования

static double ArrayForWithoutOptimization(int[] array)
{
   int sum = 0;
   var watch = Stopwatch.StartNew();
   for (int i = 0; i < array.Length; i  )
     sum  = array[i];
    watch.Stop();
    return watch.Elapsed.TotalMilliseconds;
}

static double ArrayForWithOptimization(int[] array)
{
   int length = array.Length;
   int sum = 0;
   var watch = Stopwatch.StartNew();
    for (int i = 0; i < length; i  )
      sum  = array[i];
    watch.Stop();
     return watch.Elapsed.TotalMilliseconds;
}

static double ArrayForeach(int[] array)
{
  int sum = 0;
  var watch = Stopwatch.StartNew();
   foreach (var item in array)
    sum  = item;
  watch.Stop();
  return watch.Elapsed.TotalMilliseconds;
}

static double ArrayForEach(int[] array)
{
  int sum = 0;
  var watch = Stopwatch.StartNew();
  Array.ForEach(array, i => { sum  = i; });
  watch.Stop();
  return watch.Elapsed.TotalMilliseconds;
}

Тесты выполнялись при включенном флаге оптимизировать код в Release. число элементов в массиве и списке равно 100000000. Машина, на которой проводились тесты, имеет на своём борту процессор Intel Core i-5 и 8 GB оперативной памяти.

Массивы

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

Как бы необычно это не было, но при применении массивов, кэширование длины может сказаться отрицательно. Дело в том, что когда JIT видит array.Length в качестве границы итерирования в цикле, то он переносит проверку индекса на попадание в надобные границы за цикл, тем самым проверка делается только один раз. Эту оптимизацию дюже легко порушить, и случай когда мы кэшируем переменную теснеене оптимизируется.

Способ же Array.ForEach показал самый наихудший итог. Его реализация выглядит довольно легко:

public static void ForEach<T>(T[] array, Action<T> action)
 {
  for (int index = 0; index < array.Length;   index)
    action(array[index]);
 }

Тогда отчего же он работает так медлительно? чай за кулисами он легко использует обыкновенный for. Все дело в вызове делегата action. Реально на всякой итерации вызывается способ, а мы знаем, что это лишние убыточные расходы. Тем больше, как вестимо, делегаты вызываются не так стремительно, как хотелось бы, отсель и итог.

С массивами все объяснили. Переходим к спискам, там обстановка не менее увлекательнее.

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

Списки

Вот это итог!!!

При итерации списков циклы for/foreach дают разные итоги. Дело в том, что тут нет никакой оптимизации, и foreach разворачивается в создание итератора и дальнейшую работу с ним. Но это не самое увлекательное.

Увлекательнее то, что for без оптимизации работает неторопливей foreach-а!!! Отчего это так? При применении for-а без кэширования длины списка на всякой итерации идет обращение к свойству Count (вызов способа get_Count), а так же обращение к индексатору (вызов способа get_Item). При применении итератора, то есть цикла foreach идет вызов способа MoveNext, а так же обращение к свойству Current. Получается, по два вызова способа в всяком случае. Тогда отчего один цикл неторопливей иного? Здесь в силу вступает дальнейший факт: итератором для List<>, да и вообще для обобщенных коллекций выступает изменяемая конструкция (да это зло, но разработчики пошли на это осмысленно вместо на продуктивность). Значит, ее способы вызываются с поддержкой инструкции call, в различие от callvirt, которая применяется при обращении к свойству Count и индексатору. Как вестимо, call вызывается стремительней, чем callvirt, от того что не требует проверок на null.

Способ List.ForEach показал себя с лучшей стороны, его скорость сродни скорости foreach. Его реализация выглядит так:

public void ForEach(Action<T> action)
{
  int num = this._version;
   for (int index = 0; index < this._size && num == this._version;   index)
     action(this._items[index]);
   if (num == this._version)
     return;
   ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion);
}

Тут происходит каждого один вызов делегата action, тот, что, как вестимо, осуществляется с поддержкой инструкции callvirt. В способе так же всякий раз проверяется, не изменился ли список во время итерации, и если изменился, то выбрасывается исключение.

Увлекателен факт, что у массивов способ ForEach является статическим, в то время как у списков он экземплярный. Я предполагаю, это сделано во благо увеличения продуктивности. Как вестимо, список основан на обыкновенном массиве, а потому в способе ForEach идет обращение по индексу к массиву, что в разы стремительней обращения по индексу через индексатор.

Массивы VS списки

Определенные цифры

 

  • циклы for (с и без кэширования длины) и foreach для массивов работают идентичное время;
  • цикл Array.ForEach приблизительно в два раза неторопливей циклов for/foreach;
  • for (без кэшировании длины) на списках работает приблизительно в 3 раза неторопливей, чем на массивах;
  • for (с кэшировании длины) на списках работает приблизительно в 2 раза неторопливей, чем на массивах;
  • foreach на списках неторопливей foreach на массивах приблизительно в 2 раза;
  • List.ForEach и foreach работает приблизительно идентичное время на списках;

 

Призеры

Среди массивов:

Среди списков:

В завершении

Не знаю как для Вас, но для меня эта статья оказалась дюже увлекательной. Исключительно процесс её написания.

 

Источник: programmingmaster.ru

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB