Condividi tramite


Accelerazione in PLINQ

Questo articolo fornisce informazioni utili per scrivere query PLINQ il più efficienti possibile, mantenendo comunque risultati corretti.

Lo scopo principale di PLINQ è velocizzare l'esecuzione di query LINQ to Objects eseguendo i delegati di query in parallelo su computer multicore. PLINQ offre prestazioni ottimali quando l'elaborazione di ogni elemento in una raccolta di origine è indipendente, senza alcuno stato condiviso coinvolto tra i singoli delegati. Tali operazioni sono comuni in LINQ to Objects e PLINQ e sono spesso chiamate "deliziosamente parallele" perché si prestano facilmente alla pianificazione su più thread. Tuttavia, non tutte le query sono composte da operazioni interamente parallele. Nella maggior parte dei casi, una query comporta alcuni operatori che non possono essere parallelizzati o che rallentano l'esecuzione parallela. Inoltre, anche con query completamente parallele, PLINQ deve comunque partizionare l'origine dati e pianificare il lavoro sui thread e di solito unire i risultati al termine della query. Tutte queste operazioni aggiungono al costo di calcolo della parallelizzazione; questi costi di aggiunta della parallelizzazione sono denominati overhead. Per ottenere prestazioni ottimali in una query PLINQ, l'obiettivo è ottimizzare le parti che sono deliziosamente parallele e ridurre al minimo le parti che richiedono overhead.

Fattori che influisce sulle prestazioni delle query PLINQ

Nelle sezioni seguenti sono elencati alcuni dei fattori più importanti che influiscono sulle prestazioni delle query parallele. Si tratta di istruzioni generali che da soli non sono sufficienti per stimare le prestazioni delle query in tutti i casi. Come sempre, è importante misurare le prestazioni effettive di query specifiche nei computer con una serie di configurazioni e carichi rappresentativi.

  1. Costo computazionale del lavoro complessivo.

    Per ottenere un miglioramento delle prestazioni, una query PLINQ deve avere un lavoro sufficientemente parallelo per compensare il sovraccarico. Il lavoro può essere espresso come costo di calcolo di ogni delegato moltiplicato per il numero di elementi nella raccolta di origine. Supponendo che un'operazione possa essere parallelizzata, più è dispendiosa dal punto di vista computazionale, maggiore è l'opportunità di ottenere un'accelerazione. Se ad esempio una funzione richiede un millisecondo per l'esecuzione, una query sequenziale su 1000 elementi richiederà un secondo per eseguire tale operazione, mentre una query parallela in un computer con quattro core potrebbe richiedere solo 250 millisecondi. Questo produce un'accelerazione di 750 millisecondi. Se la funzione richiedeva un secondo per l'esecuzione per ogni elemento, la velocità sarà di 750 secondi. Se il delegato è molto costoso, PLINQ potrebbe offrire un significativo aumento della velocità con solo alcuni elementi nella raccolta originale. Viceversa, le raccolte di origine di piccole dimensioni con delegati semplici non sono in genere buoni candidati per PLINQ.

    Nell'esempio seguente queryA è probabilmente un buon candidato per PLINQ, presupponendo che la relativa funzione Select comporti molte operazioni. queryB probabilmente non è un buon candidato perché nell'istruzione Select non è disponibile abbastanza lavoro e il sovraccarico della parallelizzazione sfalserà la maggior parte o tutta la velocità.

    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. Numero di core logici nel sistema (grado di parallelismo).

    Questo punto è un ovvio corollario della sezione precedente, le query che offrono parallelismo ideale vengono eseguite più velocemente sui computer con più core perché il lavoro può essere distribuito tra più thread simultanei. La quantità complessiva di accelerazione dipende da quale percentuale del lavoro complessivo della query è parallelizzabile. Tuttavia, non presupporre che tutte le query siano eseguite al doppio della velocità in un computer a otto core così come in un computer a quattro core. Quando si ottimizzano le query per ottenere prestazioni ottimali, è importante misurare i risultati effettivi nei computer con vari numeri di core. Questo punto è correlato al punto 1: i set di dati più grandi sono necessari per sfruttare i vantaggi di risorse di calcolo maggiori.

  3. Numero e tipo di operazioni.

    PLINQ fornisce l'operatore AsOrdered per le situazioni in cui è necessario mantenere l'ordine degli elementi nella sequenza di origine. C'è un costo associato all'ordinamento, ma questo costo è in genere modesto. Le operazioni GroupBy e Join comportano in modo analogo un sovraccarico. PLINQ offre prestazioni ottimali quando è consentito elaborare gli elementi nella raccolta di origine in qualsiasi ordine e passarli all'operatore successivo non appena sono pronti. Per altre informazioni, vedere Conservazione degli ordini in PLINQ.

  4. Forma di esecuzione della query.

    Se si archiviano i risultati di una query chiamando ToArray o ToList, i risultati di tutti i thread paralleli devono essere uniti nella singola struttura di dati. Ciò comporta un costo di calcolo inevitabile. Analogamente, se si esegue l'iterazione dei risultati usando un ciclo foreach (For Each in Visual Basic), i risultati dei thread di lavoro devono essere serializzati nel thread dell'enumeratore. Tuttavia, se si vuole solo eseguire un'azione in base al risultato di ogni thread, è possibile usare il metodo ForAll per eseguire questa operazione su più thread.

  5. Tipo di opzioni di unione.

    PLINQ può essere configurato per memorizzare nel buffer l'output e generarlo in blocchi o tutti contemporaneamente dopo la produzione dell'intero set di risultati oppure per trasmettere i singoli risultati man mano che vengono prodotti. Il primo comporta una diminuzione del tempo di esecuzione complessivo e quest'ultimo comporta una diminuzione della latenza tra gli elementi restituiti. Anche se le opzioni di unione non hanno sempre un impatto significativo sulle prestazioni complessive delle query, possono influire sulle prestazioni percepite perché controllano per quanto tempo un utente deve attendere per visualizzare i risultati. Per altre informazioni, vedere Opzioni di merge in PLINQ.

  6. Tipo di partizionamento.

    In alcuni casi, una query PLINQ su una raccolta di origine indicizzabile può comportare un carico di lavoro non bilanciato. In questo caso, potrebbe essere possibile aumentare le prestazioni delle query creando un partitioner personalizzato. Per ulteriori informazioni, vedere partizionatori personalizzati per PLINQ e TPL.

Quando PLINQ sceglie la modalità sequenziale

PLINQ tenterà sempre di eseguire una query almeno veloce quanto la query verrebbe eseguita in sequenza. Anche se PLINQ non esamina quanto siano costosi in termini di calcolo i delegati utente o quanto grande sia l'origine di input, esso esamina determinate "forme di query". In particolare, esso cerca operatori di query o combinazioni di operatori che in genere causano l'esecuzione di una query più lentamente in modalità parallelo. Quando trova tali forme, PLINQ per impostazione predefinita esegue il fallback alla modalità sequenziale.

Tuttavia, dopo aver misurato le prestazioni di una query specifica, è possibile determinare che viene effettivamente eseguito più velocemente in modalità parallela. In questi casi è possibile utilizzare il flag ParallelExecutionMode.ForceParallelism tramite il metodo WithExecutionMode per indicare a PLINQ di parallelizzare la query. Per altre informazioni, vedere Procedura: Specificare la modalità di esecuzione in PLINQ.

Nell'elenco seguente vengono descritte le forme di query eseguite per impostazione predefinita da PLINQ in modalità sequenziale:

  • Query che contengono una clausola Select, indexed Where, indexed SelectMany o ElementAt dopo un operatore di ordinamento o filtro che ha rimosso o riorganizzato indici originali.

  • Query che contengono un operatore Take, TakeWhile, Skip, SkipWhile e dove gli indici nella sequenza di origine non sono nell'ordine originale.

  • Le query che contengono Zip o SequenceEquals, a meno che una delle strutture dati abbia un indice originariamente ordinato e l'altra struttura dati sia indicizzabile, ad esempio un array o IList(T).

  • Query che contengono Concat, a meno che non vengano applicate alle origini dati indicizzabili.

  • Query che contengono Reverse, a meno che non vengano applicate a un'origine dati indicizzabile.

Vedere anche