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

Основное назначение PLINQ — ускорять обработку запросов LINQ to Objects на компьютерах с многоядерными процессорами, выполняя делегаты запроса в параллельном режиме.The primary purpose of PLINQ is to speed up the execution of LINQ to Objects queries by executing the query delegates in parallel on multi-core computers. Преимущества PLINQ проявляются лучше всего, когда обработка каждого элемента в исходной коллекции не зависит от других элементов и отдельные делегаты не используют общее состояние.PLINQ performs best when the processing of each element in a source collection is independent, with no shared state involved among the individual delegates. Такие операции достаточно часто встречаются в LINQ to Objects и PLINQ. Они являются параллельными, так как легко поддаются разделению на несколько потоков.Such operations are common in LINQ to Objects and PLINQ, and are often called "delightfully parallel" because they lend themselves easily to scheduling on multiple threads. Но не все запросы состоят из таких параллельных операций. Очень часто запросы содержат некоторые операторы, которые не могут выполняться параллельно или существенно замедляют параллельное выполнение.However, not all queries consist entirely of delightfully parallel operations; in most cases, a query involves some operators that either cannot be parallelized, or that slow down parallel execution. И даже для параллельных запросов PLINQ приходится выполнять дополнительную работу: разделять источники данных, распределять работу между потоками и (чаще всего) объединять результаты после обработки запроса.And even with queries that are entirely delightfully parallel, PLINQ must still partition the data source and schedule the work on the threads, and usually merge the results when the query completes. Все эти дополнительные операции привносят накладные расходы, то есть повышают вычислительную стоимость параллелизации.All these operations add to the computational cost of parallelization; these costs of adding parallelization are called overhead. Чтобы добиться оптимальной производительности запросов PLINQ, нужно применять как можно больше параллельных элементов и свести к минимуму элементы, повышающих накладные расходы.To achieve optimum performance in a PLINQ query, the goal is to maximize the parts that are delightfully parallel and minimize the parts that require overhead. Эта статья содержит сведения, которые помогут вам создавать максимально эффективные запросы PLINQ, обеспечивая правильность результатов.This article provides information that will help you write PLINQ queries that are as efficient as possible while still yielding correct results.

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

В следующих разделах перечисляются самые важные факторы, которые влияют на производительность параллельного выполнения запросов.The following sections lists some of the most important factors that impact parallel query performance. Это инструкции общего характера, которые нельзя использовать для прогнозирования производительности запросов во всех сценариях.These are general statements that by themselves are not sufficient to predict query performance in all cases. Для точной оценки нужно измерить фактическую производительность конкретных запросов на конкретных компьютерах в разных сочетаниях репрезентативных конфигураций и нагрузок.As always, it is important to measure actual performance of specific queries on computers with a range of representative configurations and loads.

  1. Вычислительная стоимость общей работы.Computational cost of the overall work.

    Чтобы ускорить обработку запросов PLINQ, нужен достаточный объем параллельных операций, позволяющий компенсировать накладные расходы.To achieve speedup, a PLINQ query must have enough delightfully parallel work to offset the overhead. Работу можно оценить как произведение вычислительной стоимости каждого делегата на число элементов в исходной коллекции.The work can be expressed as the computational cost of each delegate multiplied by the number of elements in the source collection. Если операция допускает параллелизацию, потенциал ускорения напрямую зависит от ее вычислительной стоимости.Assuming that an operation can be parallelized, the more computationally expensive it is, the greater the opportunity for speedup. Например, если функция выполняется за одну миллисекунду, последовательный запрос к 1000 элементов будет выполняться около одной секунды. Параллельное выполнение этого же запроса на компьютере с четырьмя ядрами можно произвести всего за 250 миллисекунд.For example, if a function takes one millisecond to execute, a sequential query over 1000 elements will take one second to perform that operation, whereas a parallel query on a computer with four cores might take only 250 milliseconds. Таким образом, ускорение может составить 750 миллисекунд.This yields a speedup of 750 milliseconds. Если же выполнение функции для каждого элемента занимает одну секунду, общее ускорение составит 750 секунд.If the function required one second to execute for each element, then the speedup would be 750 seconds. Если делегат является очень затратным, PLINQ может обеспечить значительное ускорение даже при небольшом размере исходной коллекции.If the delegate is very expensive, then PLINQ might offer significant speedup with only a few items in the source collection. И наоборот, небольшие исходные коллекции в сочетании с элементарными делегатами не будут хорошими кандидатами для использования PLINQ.Conversely, small source collections with trivial delegates are generally not good candidates for PLINQ.

    Запрос queryA из следующего примера можно считать хорошим кандидатом для PLINQ, если его функция Select предусматривает много работы.In the following example, queryA is probably a good candidate for PLINQ, assuming that its Select function involves a lot of work. Запрос queryB вряд ли хорошо подходит для параллелизации, так как в его инструкции Select выполняется мало работы, и накладные расходы перевесят все возможное ускорение или значительную его часть.queryB is probably not a good candidate because there is not enough work in the Select statement, and the overhead of parallelization will offset most or all of the speedup.

    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. Число логических ядер в системе (степень параллелизма).The number of logical cores on the system (degree of parallelism).

    Это очевидное следствие всего, что мы обсуждали в предыдущем разделе — параллельные запросы работают быстрее на компьютерах, которые имеют больше ядер, так как работу можно разделить между большим числом параллельных потоков.This point is an obvious corollary to the previous section, queries that are delightfully parallel run faster on machines with more cores because the work can be divided among more concurrent threads. Общий эффект ускорения зависит от того, какой процент работы поддается распараллеливанию.The overall amount of speedup depends on what percentage of the overall work of the query is parallelizable. Но не следует полагать, что на компьютере с восемью ядрами все запросы будут выполняться в два раза быстрее,чем на компьютере с четырьмя ядрами.However, do not assume that all queries will run twice as fast on an eight core computer as a four core computer. При оптимизации производительности запросов важно измерить фактические результаты на компьютерах с разным количеством ядер.When tuning queries for optimal performance, it is important to measure actual results on computers with various numbers of cores. Этот аспект напрямую связан с аспектом № 1: увеличение вычислительных ресурсов принесет больше пользы для крупных наборов данных.This point is related to point #1: larger datasets are required to take advantage of greater computing resources.

  3. Число и типы операций.The number and kind of operations.

    Язык PLINQ предоставляет оператор AsOrdered для ситуаций, в которых важно поддерживать исходный порядок элементов в последовательности.PLINQ provides the AsOrdered operator for situations in which it is necessary to maintain the order of elements in the source sequence. Упорядочение требует определенных затрат, но обычно они не очень велики.There is a cost associated with ordering, but this cost is usually modest. Операции GroupBy и Join требуют затрат.GroupBy and Join operations likewise incur overhead. PLINQ работает лучше всего, если есть возможность обрабатывать элементы исходной коллекции в любом порядке и передавать результаты следующему оператору сразу по мере готовности.PLINQ performs best when it is allowed to process elements in the source collection in any order, and pass them to the next operator as soon as they are ready. Дополнительные сведения см. в разделе Сохранение порядка в PLINQ.For more information, see Order Preservation in PLINQ.

  4. Форма выполнения запроса.The form of query execution.

    Если вы сохраняете результаты запроса вызовом ToArray или ToList, все результаты из всех параллельных потоков необходимо объединять в одну структуру данных.If you are storing the results of a query by calling ToArray or ToList, then the results from all parallel threads must be merged into the single data structure. С этим процессом связаны неизбежные вычислительные затраты.This involves an unavoidable computational cost. Если же результаты просматриваются в цикле foreach (For Each в Visual Basic), результаты из рабочих потоков нужно сериализовать в поток-перечислитель.Likewise, if you iterate the results by using a foreach (For Each in Visual Basic) loop, the results from the worker threads need to be serialized onto the enumerator thread. Но если вам нужно лишь выполнить некоторую операцию над результатами каждого потока, вы можете использовать метод ForAll, поддерживающий многопоточное выполнение.But if you just want to perform some action based on the result from each thread, you can use the ForAll method to perform this work on multiple threads.

  5. Тип параметров слияния.The type of merge options.

    В PLINQ можно включить буферизацию, чтобы возвращать результаты блоками или целиком после завершения работы, или выводить каждый отдельный результат сразу по мере готовности.PLINQ can be configured to either buffer its output, and produce it in chunks or all at once after the entire result set is produced, or else to stream individual results as they are produced. Первый вариант уменьшает общее время выполнения, а второй снижает задержку до получения приостановленных элементов.The former results in decreased overall execution time and the latter results in decreased latency between yielded elements. Хотя параметры слияния не всегда значительно влияют на общую производительность запроса, они изменяют субъективное восприятие пользователя, так как от этого выбора напрямую зависит, сколько времени пользователю придется ждать до начала вывода результатов.While the merge options do not always have a major impact on overall query performance, they can impact perceived performance because they control how long a user must wait to see results. Дополнительные сведения см. в разделе Параметры слияние в PLINQ.For more information, see Merge Options in PLINQ.

  6. Тип секционирования.The kind of partitioning.

    В некоторых случаях запрос PLINQ по индексируемой исходной коллекции может привести к несбалансированности рабочей нагрузки.In some cases, a PLINQ query over an indexable source collection may result in an unbalanced work load. В этом случае производительность запросов можно увеличить, добавив пользовательский модуль разделения.When this occurs, you might be able to increase the query performance by creating a custom partitioner. Дополнительные сведения см. в разделе Пользовательские разделители для PLINQ и TPL.For more information, see Custom Partitioners for PLINQ and TPL.

В каких случаях PLINQ выбирает последовательный режимWhen PLINQ Chooses Sequential Mode

PLINQ всегда старается выполнять запрос по меньшей мере так же быстро, как если бы он выполнялся последовательно.PLINQ will always attempt to execute a query at least as fast as the query would run sequentially. PLINQ не оценивает, сколько вычислительных ресурсов потребуется для пользовательских делегатов, и не учитывает размер источника входных данных, но проверяет некоторые характерные формы запросов.Although PLINQ does not look at how computationally expensive the user delegates are, or how big the input source is, it does look for certain query "shapes." В частности, определяется наличие в запросе некоторых операторов или их сочетаний, которые часто приводят к медленному выполнению в параллельном режиме.Specifically, it looks for query operators or combinations of operators that typically cause a query to execute more slowly in parallel mode. Обнаружив некоторые из таких форм, PLINQ по умолчанию переходит в последовательный режим.When it finds such shapes, PLINQ by default falls back to sequential mode.

Но иногда, оценив производительность конкретного запроса, вы заметите, что он все таки выполняется быстрее в параллельном режиме.However, after measuring a specific query's performance, you may determine that it actually runs faster in parallel mode. В таких случаях можно использовать флаг ParallelExecutionMode.ForceParallelism в методе WithExecutionMode, чтобы принудительно указать для PLINQ параллельный режим выполнения запроса.In such cases you can use the ParallelExecutionMode.ForceParallelism flag via the WithExecutionMode method to instruct PLINQ to parallelize the query. Дополнительные сведения см. в разделе Практическое руководство. Задание режима выполнения в PLINQ.For more information, see How to: Specify the Execution Mode in PLINQ.

В следующем списке описываются формы запросов, которые PLINQ по умолчанию будет выполнять в последовательном режиме.The following list describes the query shapes that PLINQ by default will execute in sequential mode:

  • Запросы, содержащие предложение Select, а также индексированные инструкции Where, SelectMany или ElementAt после оператора упорядочивания или фильтрации, который удаляет или изменяет исходные индексы.Queries that contain a Select, indexed Where, indexed SelectMany, or ElementAt clause after an ordering or filtering operator that has removed or rearranged original indices.

  • Запросы, содержащие оператор Take, TakeWhile, Skip или SkipWhile, в которых индексы исходной последовательности не сохраняют исходный порядок.Queries that contain a Take, TakeWhile, Skip, SkipWhile operator and where indices in the source sequence are not in the original order.

  • Запросы, которые содержат Zip или SequenceEquals, за исключением случаев, когда один из источников данных содержит изначально упорядоченный индекс, а другой источник данных можно проиндексировать (например, массив или IList(T)).Queries that contain Zip or SequenceEquals, unless one of the data sources has an originally ordered index and the other data source is indexable (i.e. an array or IList(T)).

  • Запросы, которые содержат оператор Concat, если он не применяется к индексируемым источникам данных.Queries that contain Concat, unless it is applied to indexable data sources.

  • Запросы, содержащие оператор Reverse, если он не применяется к индексируемым источникам данных.Queries that contain Reverse, unless applied to an indexable data source.

См. такжеSee also