Когда использовать, для чего? - PullRequest
6 голосов
/ 19 мая 2011

РЕДАКТИРОВАТЬ Дополнительные опции и несколько расширенный вопрос ниже.

Рассмотрим этот надуманный и абстрактный пример тела класса.Он демонстрирует четыре различных способа выполнения итерации «за».

private abstract class SomeClass
{
    public void someAction();
}

void Examples()
{
    List<SomeClass> someList = new List<SomeClass>();

    //A. for
    for (int i = 0; i < someList.Count(); i++)
    {
        someList[i].someAction();
    }

    //B. foreach
    foreach (SomeClass o in someList)
    {
        o.someAction();
    }

    //C. foreach extension
    someList.ForEach(o => o.someAction());

    //D. plinq
    someList.AsParallel().ForAll(o => o.someAction());

РЕДАКТИРОВАТЬ: Добавление некоторых вариантов из ответов и исследований.

    //E. ParallelEnumerable
    ParallelEnumerable.Range(0, someList.Count - 1)
        .ForAll(i => someList[i].someAction());

    //F. ForEach Parallel Extension
    Parallel.ForEach(someList, o => o.someAction());

    //G. For Parallel Extension
    Parallel.For(0, someList.Count - 1, i => someList[i].someAction())
}

Мой вопроспоставляется в двух частях.Я пропустил какой-то важный вариант?Какой вариант является лучшим выбором, учитывая удобочитаемость, но в первую очередь производительность?

Пожалуйста, укажите, может ли сложность реализации SomeClass или Count из someList повлиять на этот выбор.

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

Как соломенный человек.Я подозреваю, что во всех реализациях SomeClass и всех длинах someList опция //E. ParallelEnumerable обеспечит наилучшую среднюю производительность, учитывая преобладание многопроцессорных архитектур.Я не проводил никакого тестирования, чтобы доказать это.

Примечание. Для параллельных расширений потребуется использовать пространство имен System.Threading.Tasks.

Ответы [ 7 ]

6 голосов
/ 19 мая 2011

Опция A действительно имеет смысл только для последовательностей, которые реализуют индексацию, и будет эффективна только для тех, которые имеют O(1) время поиска.Как правило, я бы использовал foreach и варианты, если у вас нет специальной логики.

Также обратите внимание, что "специальная логика", такая как for (int i = 1; i < list.Count; i++), может быть реализована с помощью методов расширения Linq: foreach(var item in sequence.Skip(1)).

Итак, обычно предпочитают B над A.

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

Что касается D: Этобудет зависеть от множества факторов.Я предполагаю, что для простых вычислений вы не хотите этого делать - вы получите пользу от распараллеливания, только если тело цикла требует времени для вычисления.

3 голосов
/ 19 мая 2011

IL показывает нам, что цикл for является наиболее эффективным.Конечный автомат беспокоиться не о чем.

для выдает следующее

IL_0036:  br.s        IL_0048
IL_0038:  ldloc.0     
IL_0039:  ldloc.1     
IL_003A:  callvirt    System.Collections.Generic.List<UserQuery+SomeClass>.get_Item
IL_003F:  callvirt    UserQuery+SomeClass.someAction
IL_0044:  ldloc.1     
IL_0045:  ldc.i4.1    
IL_0046:  add         
IL_0047:  stloc.1     
IL_0048:  ldloc.1     
IL_0049:  ldloc.0     
IL_004A:  call        System.Linq.Enumerable.Count
IL_004F:  blt.s       IL_0038

IL_0051: ret

IL, выдаваемый здесь для foreach показывает конечный автомат на работе.Версия LINQ и ForEach выдают схожий результат.

IL_0035:  callvirt    System.Collections.Generic.List<UserQuery+SomeClass>.GetEnumerator
IL_003A:  stloc.3     
IL_003B:  br.s        IL_004B
IL_003D:  ldloca.s    03 
IL_003F:  call        System.Collections.Generic.List<UserQuery+SomeClass>.get_Current
IL_0044:  stloc.1     
IL_0045:  ldloc.1     
IL_0046:  callvirt    UserQuery+SomeClass.someAction
IL_004B:  ldloca.s    03 
IL_004D:  call        System.Collections.Generic.List<UserQuery+SomeClass>.MoveNext
IL_0052:  brtrue.s    IL_003D
IL_0054:  leave.s     IL_0064
IL_0056:  ldloca.s    03 
IL_0058:  constrained. System.Collections.Generic.List<>.Enumerator
IL_005E:  callvirt    System.IDisposable.Dispose
IL_0063:  endfinally  
IL_0064:  ret   

Я не проводил никаких тестов, но думаю, что это безопасное предположение.

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

Не думаю, что я бы сравнил AsParallel () с for, foreach или лямбда-эквиваленты.Вы бы разделяли задачи с интенсивным использованием ЦП или блокировали операции, используя AsParallel (), вы бы не использовали его просто для перебора «нормальной» коллекции.

3 голосов
/ 19 мая 2011

вы пропустили:

Parallel.ForEach(someList, o => o.someAction())
Parallel.For(0, someList.Length, i => someList[i].someAction())
1 голос
/ 19 мая 2011

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

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

using(var myEnum = aList.GetEnumerator()){
    while(myEnum.MoveNext()){
        myEnum.Current.SomeAction();
    }
}
1 голос
/ 19 мая 2011

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

  //A. for
    for (int i = 0; i < someList.Count(); i++)
    {
        someList[i].someAction();
    }

или

 //D. plinq
    someList.AsParallel().ForAll(o => o.someAction());

Хотя в случае с A я бы предпочел не делать someList.Count () каждый раз.

for работает лучшепо сравнению с foreach, что касается производительности.D может быть лучше, чем A, но это будет зависеть от сценария.Если у вас есть большие данные в некотором списке, параллелизм может помочь, но если у вас маленькие данные, это может вызвать дополнительную нагрузку

0 голосов
/ 19 мая 2011

За исключением (C), который, кажется, читает мне задом наперед, я могу вспомнить ситуации, в которых вы, возможно, захотите использовать каждый из остальных.Кроме того, в зависимости от того, что вы делаете, вы также можете добавить стандартный микс LINQ.Например, если ваш цикл просто использует элемент списка для создания какого-либо другого объекта.

 (E) var someOtherCollection = someList.Select( l => transform(l) );

Для опции (A), если вам нужно знать позицию в списке, а также использование элемента.Вариант (B) или (E) будет тем, что я обычно использую.Опция (D) имеет смысл, если список большой и действия поддаются распараллеливанию (нет или управляемые зависимости между элементами).

Поскольку вы используете общий список, все, кроме (E), являются O(Н).Count () должна быть операцией O (1), поскольку она хранится внутри переменной.Для других перечислимых типов вам нужно знать, как строится структура данных.Если вы не знаете тип коллекции, я бы использовал реализацию foreach или LINQ над индексированной реализацией, поскольку коллекция не может быть проиндексирована, и это может превратить ваше перечисление в O (N 2 *).1008 *) операция.

0 голосов
/ 19 мая 2011

for(int i = 0...) Чтобы использовать эту методологию, у вас должен быть массив, к которому вы можете обращаться каждый элемент, один за другим.

foreach (SomeClass o in someList) Этот синтаксис может использоваться для перечислимого класса, класса, который реализует IEnumerable. IEnumerable имеет метод GetEnumerator(), который знает, как пройти через каждый элемент коллекции.Теперь вышеприведенный массив реализует IEnumerable.Способ, которым он знает, как перечислять через коллекцию, - это то, как вы определили это выше.Однако не все IEnumerable классы, которые могут использовать синтаксис foreach, могут использовать первый метод, поскольку не все коллекции предоставляют доступ к каждому элементу.Рассмотрим следующую функцию (не проверял ее):

public IEnumerable<int> GetACoupleOfInts()
{
yield return 1;
yield return 2;
}

}

Этот метод позволит вам использовать конструкцию foreach, так как среда выполнения знает, как перечислять череззначения GetACoupleInts(), но не позволяют конструкцию for.

someList.ForEach(o => o.someAction()); - Как я понимаю, эта лямбда будет просто преобразована в то же выражение, что и foreach (SomeClass o in someList)

someList.AsParallel().ForAll(o => o.someAction()); - Решая, использовать ли PLINQ или нет, вы должны решить, стоит ли "Сок стоит выжать".Если объем работы в someAction() является тривиальным, то затраты времени выполнения при организации всех данных из одновременных действий будут ЧЕГО слишком высоки, и вам будет лучше делать это последовательно.

tl; dr - Первые три, скорее всего, приведут к одним и тем же вызовам и не окажут реального влияния на производительность, хотя они имеют разные значения в рамках.Четвертый вариант требует более тщательного рассмотрения перед использованием.

...