Прочитать на английском

Поделиться через


Ускорение работы в PLINQ

В этой статье содержатся сведения, которые помогут вам создавать запросы PLINQ, которые максимально эффективны, при этом по-прежнему дают правильные результаты.

Основной целью PLINQ является ускорение выполнения запросов LINQ to Objects путем параллельного выполнения делегатов запросов на многоядерных компьютерах. PLINQ лучше всего работает, когда обработка каждого элемента в исходной коллекции независима и не включает общего состояния среди отдельных делегатов. Такие операции распространены в LINQ to Objects и PLINQ, и часто называются "восхитительно параллельными", так как они позволяют легко планировать на нескольких потоках. Однако не все запросы состоят полностью из восхитительно параллельных операций. В большинстве случаев запрос включает в себя некоторые операторы, которые не могут быть параллелизированы или замедляют параллельное выполнение. И даже с запросами, которые полностью восхитительно параллельны, PLINQ все еще должен разделять источник данных и распределять работу над потоками, и обычно сливать результаты после выполнения запроса. Все эти операции добавляются к вычислительной стоимости параллелизации; эти затраты на добавление параллелизации называются издержками. Чтобы достичь оптимальной производительности в запросе PLINQ, цель состоит в том, чтобы максимизировать части, которые идеально подходят для параллельной обработки, и свести к минимуму части, требующие дополнительных ресурсов.

Факторы, влияющие на производительность запросов PLINQ

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

  1. Вычислительные затраты на общую работу.

    Чтобы ускорить скорость, запрос PLINQ должен иметь достаточно восхитительно параллельной работы, чтобы компенсировать затраты. Работа может быть выражена в виде вычислительной стоимости каждого делегата, умноженного на количество элементов в исходной коллекции. Предположим, что операцию можно параллелизовать: чем выше вычислительные затраты, тем больше возможности для ускорения. Например, если функция принимает один миллисекунд для выполнения, последовательный запрос более 1000 элементов займет одну секунду для выполнения этой операции, в то время как параллельный запрос на компьютере с четырьмя ядрами может занять только 250 миллисекунд. Это дает ускорение на 750 миллисекунд. Если для каждого элемента требуется одна секунда, скорость будет составлять 750 секунд. Если делегат очень ресурсоемкий, PLINQ может предложить значительное ускорение даже при нескольких элементах в исходной коллекции. И наоборот, небольшие исходные коллекции с тривиальными делегатами обычно не являются хорошими кандидатами для PLINQ.

    В следующем примере queryA, вероятно, является хорошим кандидатом для PLINQ, предполагая, что ее функция Select включает много работы. queryB, вероятно, не является хорошим кандидатом, так как в операторе Select недостаточно работы, а издержки параллелизации будут сводить на нет большую часть или все полученное ускорение.

    C#
    var queryA = from num in numberList.AsParallel()  
                 select ExpensiveFunction(num); //good for PLINQ  
    
    var queryB = from num in numberList.AsParallel()  
                 where num % 2 > 0  
                 select num; //not as good for PLINQ  
    
  2. Количество логических ядер в системе (степень параллелизма).

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

  3. Число и тип операций.

    PLINQ предоставляет оператор AsOrdered для ситуаций, в которых необходимо сохранить порядок элементов в исходной последовательности. Существует стоимость, связанная с заказом, но эта стоимость обычно скромна. Операции GroupBy и Join также повысят нагрузку. PLINQ лучше всего выполняется, если разрешено обрабатывать элементы в исходной коллекции в любом порядке и передавать их следующему оператору сразу после их готовности. Дополнительные сведения см. в разделе "Сохранение заказа" в PLINQ.

  4. Форма выполнения запроса.

    Если вы храните результаты запроса путем вызова ToArray или ToList, результаты из всех параллельных потоков должны быть объединены в одну структуру данных. Это связано с неизбежной вычислительной стоимостью. Аналогичным образом, если вы выполняете итерацию результатов с помощью цикла foreach (For Each in Visual Basic), результаты рабочих потоков должны быть сериализованы в поток перечислителя. Но если вы просто хотите выполнить некоторые действия на основе результата из каждого потока, можно использовать метод ForAll для выполнения этой работы с несколькими потоками.

  5. Тип параметров слияния.

    PLINQ можно настроить для буферизации выходных данных, чтобы создавать их блоками или сразу после формирования всего результирующего набора, либо передавать отдельные результаты по мере их создания. Первый результат уменьшает общее время выполнения, а последний приводит к уменьшению задержки между предоставленными элементами. Хотя параметры слияния не всегда оказывают большое влияние на общую производительность запросов, они могут повлиять на воспринимаемую производительность, так как они управляют тем, сколько времени пользователь должен ждать, чтобы увидеть результаты. Дополнительные сведения см. в разделе "Параметры слияния" в PLINQ.

  6. Тип секционирования.

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

Если PLINQ выбирает последовательный режим

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

Однако после измерения производительности конкретного запроса можно определить, что он фактически выполняется быстрее в параллельном режиме. Можно использовать флаг ParallelExecutionMode.ForceParallelism через метод WithExecutionMode, чтобы указать PLINQ параллелизировать запрос. Дополнительные сведения см. в разделе "Практическое руководство. Указание режима выполнения в PLINQ".

В следующем списке описываются формы запросов, которые PLINQ по умолчанию выполняет в последовательном режиме:

  • Запросы, содержащие предложение Select, индексированное Where, индексированное SelectMany или ElementAt после оператора упорядочивания или фильтрации, удаляющего или переупорядочивающего исходные индексы.

  • Запросы, содержащие оператор Take, TakeWhile, Skip, SkipWhile и где индексы в исходной последовательности не находятся в исходном порядке.

  • Запросы, содержащие Zip или SequenceEquals, если один из источников данных не имеет первоначально упорядоченного индекса, а другой источник данных индексируется (т. е. массив или IList(T)).

  • Запросы, содержащие Concat, если они не применяются к индексируемым источникам данных.

  • Запросы, содержащие условие "Reverse", если не применяется к индексируемому источнику данных.

См. также


Дополнительные ресурсы

Обучение

Модуль

Реализация асинхронных задач - Training

Узнайте, как реализовать асинхронные задачи в приложениях C# с помощью ключевых слов async и await и параллельного выполнения асинхронных задач.

События

MCP DevDays

29 июл., 02 - 29 июл., 02

Ускорение производительности, создание будущего

Register Today