Aceleração no PLINQ

Este artigo fornece informações que irão ajudá-lo a escrever consultas PLINQ que são tão eficientes quanto possível, produzindo resultados corretos.

O principal objetivo do PLINQ é acelerar a execução de consultas LINQ to Objects executando os delegados de consulta em paralelo em computadores multi-core. PLINQ funciona melhor quando o processamento de cada elemento em uma coleção de origem é independente, sem estado compartilhado envolvido entre os delegados individuais. Tais operações são comuns em LINQ to Objects e PLINQ, e são frequentemente chamadas de "deliciosamente paralelas" porque se prestam facilmente ao agendamento em vários threads. No entanto, nem todas as consultas consistem inteiramente em operações deliciosamente paralelas. Na maioria dos casos, uma consulta envolve alguns operadores que não podem ser paralelizados ou que retardam a execução paralela. E mesmo com consultas que são totalmente deliciosamente paralelas, PLINQ ainda deve particionar a fonte de dados e agendar o trabalho nos threads, e geralmente mesclar os resultados quando a consulta é concluída. Todas estas operações aumentam o custo computacional da paralelização; Esses custos de adição de paralelização são chamados de despesas gerais. Para alcançar o melhor desempenho em uma consulta PLINQ, o objetivo é maximizar as peças que são deliciosamente paralelas e minimizar as peças que exigem sobrecarga.

Fatores que afetam o desempenho da consulta PLINQ

As seções a seguir listam alguns dos fatores mais importantes que afetam o desempenho da consulta paralela. Estas são instruções gerais que, por si só, não são suficientes para prever o desempenho da consulta em todos os casos. Como sempre, é importante medir o desempenho real de consultas específicas em computadores com uma variedade de configurações e cargas representativas.

  1. Custo computacional do trabalho global.

    Para obter velocidade, uma consulta PLINQ deve ter trabalho paralelo agradável suficiente para compensar a sobrecarga. O trabalho pode ser expresso como o custo computacional de cada delegado multiplicado pelo número de elementos na coleção de origem. Supondo que uma operação possa ser paralelizada, quanto mais cara computacionalmente for, maior será a oportunidade de aceleração. Por exemplo, se uma função leva um milissegundo para ser executada, uma consulta sequencial acima de 1000 elementos levará um segundo para executar essa operação, enquanto uma consulta paralela em um computador com quatro núcleos pode levar apenas 250 milissegundos. Isso produz uma velocidade de 750 milissegundos. Se a função exigisse um segundo para ser executada para cada elemento, então a aceleração seria de 750 segundos. Se o delegado for muito caro, então o PLINQ pode oferecer uma aceleração significativa com apenas alguns itens na coleção de fontes. Por outro lado, pequenas coleções de fontes com delegados triviais geralmente não são bons candidatos para PLINQ.

    No exemplo a seguir, queryA é provavelmente um bom candidato para PLINQ, assumindo que sua função Select envolve muito trabalho. queryB provavelmente não é um bom candidato porque não há trabalho suficiente na instrução Select e a sobrecarga de paralelização compensará a maior parte ou toda a aceleração.

    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. O número de núcleos lógicos no sistema (grau de paralelismo).

    Este ponto é um corolário óbvio para a seção anterior, consultas que são deliciosamente paralelas são executadas mais rapidamente em máquinas com mais núcleos porque o trabalho pode ser dividido entre threads mais simultâneos. A quantidade geral de aceleração depende de qual porcentagem do trabalho geral da consulta é paralelizável. No entanto, não assuma que todas as consultas serão executadas duas vezes mais rápido em um computador de oito núcleos do que em um computador de quatro núcleos. Ao ajustar consultas para um desempenho ideal, é importante medir os resultados reais em computadores com vários números de núcleos. Este ponto está relacionado com o ponto #1: conjuntos de dados maiores são necessários para tirar proveito de maiores recursos de computação.

  3. O número e o tipo de operações.

    PLINQ fornece o operador AsOrdered para situações em que é necessário manter a ordem dos elementos na sequência de origem. Existe um custo associado à encomenda, mas este custo é geralmente modesto. As operações GroupBy e Join também incorrem em despesas gerais. PLINQ funciona melhor quando é permitido processar elementos na coleção de origem em qualquer ordem, e passá-los para o próximo operador assim que estiverem prontos. Para obter mais informações, consulte Preservação de pedidos no PLINQ.

  4. A forma de execução da consulta.

    Se você estiver armazenando os resultados de uma consulta chamando ToArray ou ToList, os resultados de todos os threads paralelos deverão ser mesclados na estrutura de dados única. Isso envolve um custo computacional inevitável. Da mesma forma, se você iterar os resultados usando um loop foreach (For Each no Visual Basic), os resultados dos threads de trabalho precisam ser serializados no thread do enumerador. Mas se você quiser apenas executar alguma ação com base no resultado de cada thread, você pode usar o método ForAll para executar esse trabalho em vários threads.

  5. O tipo de opções de mesclagem.

    O PLINQ pode ser configurado para armazenar em buffer sua saída e produzi-la em partes ou de uma só vez depois que todo o conjunto de resultados é produzido, ou então para transmitir resultados individuais à medida que são produzidos. O primeiro resulta na diminuição do tempo de execução global e o segundo resulta na diminuição da latência entre os elementos produzidos. Embora as opções de mesclagem nem sempre tenham um grande impacto no desempenho geral da consulta, elas podem afetar o desempenho percebido porque controlam quanto tempo um usuário deve esperar para ver os resultados. Para obter mais informações, consulte Opções de mesclagem no PLINQ.

  6. O tipo de particionamento.

    Em alguns casos, uma consulta PLINQ sobre uma coleção de fontes indexáveis pode resultar em uma carga de trabalho desequilibrada. Quando isso ocorre, você poderá aumentar o desempenho da consulta criando um particionador personalizado. Para obter mais informações, consulte Particionadores personalizados para PLINQ e TPL.

Quando PLINQ escolhe o modo sequencial

O PLINQ sempre tentará executar uma consulta pelo menos tão rápido quanto a consulta seria executada sequencialmente. Embora o PLINQ não analise o quão caro computacionalmente os delegados do usuário são, ou quão grande é a fonte de entrada, ele procura certas "formas" de consulta. Especificamente, ele procura operadores de consulta ou combinações de operadores que normalmente fazem com que uma consulta seja executada mais lentamente no modo paralelo. Quando encontra tais formas, PLINQ por padrão cai de volta para o modo sequencial.

No entanto, depois de medir o desempenho de uma consulta específica, você pode determinar que ela realmente é executada mais rapidamente no modo paralelo. Nesses casos, você pode usar o sinalizador ParallelExecutionMode.ForceParallelism através do WithExecutionMode método para instruir o PLINQ a paralelizar a consulta. Para obter mais informações, consulte Como especificar o modo de execução no PLINQ.

A lista a seguir descreve as formas de consulta que o PLINQ, por padrão, executará no modo sequencial:

  • Consultas que contêm uma cláusula Select, indexed Where, indexed SelectMany ou ElementAt após um operador de ordenação ou filtragem que removeu ou reorganizou os índices originais.

  • Consultas que contêm um operador Take, TakeWhile, Skip, SkipWhile e onde os índices na sequência de origem não estão na ordem original.

  • Consultas que contêm Zip ou SequenceEquals, a menos que uma das fontes de dados tenha um índice ordenado originalmente e a outra fonte de dados seja indexável (ou seja, uma matriz ou IList(T)).

  • Consultas que contêm Concat, a menos que ele seja aplicado a fontes de dados indexáveis.

  • Consultas que contêm Reverso, a menos que aplicadas a uma fonte de dados indexável.

Consulte também