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

Что стремительней? foreach vs. List.ForEach vs for-loop

Anna | 17.06.2014 | нет комментариев
Сегодня (прим. переводчика: т.е. 6 лет назад) я перебирал список List, применяя конструкцию foreach, и Ощущая малое самодовольство, осмысливая насколько это продуктивнее, того что было бы, попытайся я применять ArrayList. Вследствие чуду Generic компилятор C# старательно чурается бесчисленные упаковочные операции с поддержкой экземпляров System.Collections.Generic.IEnumerator<int> взамен ветхихSystem.Collections.IEnumerator. Тогда я подумал: «подлинно ли это самый стремительный метод?» По итогам расследования, получается, что, нет, это не самый стремительный метод.

NET Framework 2.0 (прим. переводчика: да, статья ветхая) полон маленьких вкусняшек, которые делают жизнь легче, а код изящнее. Среди моих любимых, добавочные способы для массивов и списков, такие какAction<T>Converter<TInput,TOutput> и Predicate<T>, которые принимают создаваемые делегаты. Судя по каждому, я одержим ими. Эти способы подлинно сияют, когда применяются с неизвестными способами и лямбда выражениями. В частности, я заинтересовался способом List<T>.ForEach(Action<T>). Я захотел узнать, ForEach неторопливей, стремительней либо приблизительно такой же по продуктивности как и типовой Foreach?
Разглядим данный код:

long Sum(List<int> intList)
{
  long result = 0;
  foreach (int i in intList)
    result  = i;
  return result;
}

Компилятор C# будет генерировать IL инструкцию для приведенного выше кода, тот, что транслируется приблизительно так:

long Sum(List<int> intList)
{
  long result = 0;
  List<T>.Enumerator enumerator = intList.GetEnumerator();
  try
  {
    while (enumerator.MoveNext())
    {
      int i = enumerator.Current;
      result  = i;
    }
  }
  finally
  {
    enumerator.Dispose();
  }
  return result;
}

По сути, компилятор C # генерирует два вызываемых способа за одну итерацию: IEnumerator <T>.MoveNext () и IEnumerator <T>.Current. Применение конструкции List<T>.Enumerator (которая реализует IEnumerator) разрешает компилятору генерировать вызываемые IL инструкции взамен callvirt, Дабы обеспечить маленький приход скорости на нижнем ярусе.
Для многообразия, разглядим данный код:

long Sum(List<int> intList)
{
  long result = 0;
  intList.ForEach(delegate(int i) { result  = i; });
  result result;
}
Либо тот же код, на использующий лямбда выражения:
long Sum(List<int> intList)
{
  long result = 0;
  intList.ForEach(i => result  = i);
  return result;
}

В итоге, применяя List<T>.ForEach создаётся только один способ за итерацию, где Action<Т> всякий делегат, тот, что вы предоставляете. Он будет вызывать callvirt IL инструкцию, но два вызова неторопливей, чем один callvirt. Выходит, я жду, что List<T>.ForEach на самом деле будет стремительней.
Вооруженный моим предположениям, я сотворил малое консольного приложения, которое замеряло время суммирования всех элементов списка List<int> для четырёх разных методов перебора:

  1. for (int i = 0; i < List<int>.Count; i )
  2. for (int i = 0; i < NUM_ITEMS; i )
  3. foreach (int i in List<int>)
  4. List<int>.ForEach(delegate(int i) { result = i; });

Первые, итоги я сгенерировал без оптимизации компилятора:
image
Эти итоги немыслимы. Оказывается, что без оптимизации, List<int>.ForEach даже стремительней, чем типовые конструкции! Не меньший интерес представляет, что (без оптимизации компилятора) Foreach примерно так же стремителен, как типовые циклы. Выходит, если вы собираете приложение, без оптимизации компилятора, то List<int>.ForEach является самым стремительным способом.
Дальше, я прогнал мои тесты с помощью оптимизации компилятора, Дабы получить итоги приближенные к «реальному миру»:
image
Глядя на эти цифры, я находился под впечатлен от того, как результативно компилятор оптимизирует циклы, Дабы получить больше, чем 50% экономии. ForEach-петлям также удалось урезать около 20%.List<T>.ForEach не оптимизируется так отлично, но основное, что он по-бывшему обгоняет Foreach-петли со существенным отрывом. List<T>.ForEach стремительней, чем типовой Foreach.

Подмечу, что эти тесты проводились на ноутбуке Dell Inspiron 9400 с Core Duo T2400 и 2 Гб оперативной памяти. Если вы хотите прогнать эти тесты у себя, вы можете скачать консольное приложение тут:ForEachTest.zip (3,82 Кб).
И так как одна картинка стоит тысячи слов, я сотворил пару графиков, Дабы продемонстрировать отличия в скорости тестов. На графиках показано 5 разных примеров, разрешающие перебирать 10000000 в 50000000 целых значений.

Без оптимизации компилятора:
image

С оптимизацией компилятора:
image

И наконец: в то время как способ ForEach гораздо стремительней для перебора List<T>, это не применимо для случаев с массивами. Способ Array.ForEach существует для одномерных массивов (либо векторов) и он на самом деле неторопливей, чем применяемый типовой foreach. Повод в том, что компилятор не генерирует код, использующий IEnumerator<T> для foreach-петель через векторы. Взамен этого, он генерирует особые инструкции IL, которые предуготовлены для работы с векторами. Таким образом, применяя foreach на векторах, способы в результате не вызываются, в то время как Array.ForEach еще требует один вызов для предоставленного делегата.

Примечание переводчика:
Т.к. для написания скриптов на Unity3D я использую C#, то я не сумел устоять от соблазна проверки описанных итогов на практике. Я написал маленький

скрипт

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class testspeed : MonoBehaviour {

	public enum LoopType {none, for_loop, for_loop_no_count, foreach_loop, ListintForEach}
	public LoopType type;
	public int Count=2;
	public int iterationCount=1000;
	private List<int> lint = new List<int>();
	private int[] alint;
	private long result;

	void Start () {
		for (int i = 0 ; i<Count; i  ) {int r=UnityEngine.Random.Range(0,1000); lint.Add(r);}
	}

	void Update () {
		for (int j =0; j<iterationCount;j  )
		{
			result=0;
			switch (type)
			{
			case LoopType.for_loop: for (int i = 0; i < lint.Count; i  ) {result  = lint[i];} break;
			case LoopType.for_loop_no_count: int c = lint.Count; for (int i = 0; i < c; i  ){result  = lint[i];} break;
			case LoopType.foreach_loop: foreach (int i in lint) {result  = i;} break;
			case LoopType.ListintForEach: lint.ForEach(i => result  = i); break;
			}
		}

	}
}

В итоге, я подметил одно хитрость в приведённых выше данных, заключающееся в том, что нижней рубежом для таблицы выбрано 1000 членов списка. Если список содержит в себе огромнее 1000 элементов, то всё вышеописанное правильно. Но у меня на практике такое такое встречается не Зачастую, традиционно список ограничивается десятками, ну может сотнями элементов.
Следственно я привожу свою таблицу:
image
Как видно, для маленьких списков отменнее каждого применять типовой цикл с внешним счётчиком. По продуктивности ForEach проигрывает ему в разы.

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