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


Общее представление об ускорении выполнения в PLINQ

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

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

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

  1. Вычислительная стоимость общей работы.

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

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

    Dim queryA = From num In numberList.AsParallel()
                 Select ExpensiveFunction(num); 'good for PLINQ
    
    Dim queryB = From num In numberList.AsParallel()
                 Where num Mod 2 > 0
                 Select num; 'not as good for PLINQ
    
    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 в Visual Basic), результаты из рабочих потоков должны быть сериализованы в поток-перечислитель. Однако если необходимо выполнить отдельные действия на основе результатов из каждого потока, для выполнения этой работы в нескольких потоках можно использовать метод ForAll.

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

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

  6. Тип разделения.

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

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

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

Однако после оценки производительности определенного запроса можно определить, что он фактически выполняется быстрее в параллельном режиме. В таких случаях можно использовать флаг ParallelExecutionMode.ForceParallelism с помощью метода ParallelEnumerableWithExecutionMode() для указания PLINQ выполнять запрос в параллельном режиме. Дополнительные сведения см. в разделе Практическое руководство. Задание режима выполнения в PLINQ.

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

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

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

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

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

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

См. также

Основные понятия

Parallel LINQ (PLINQ)