Freigeben über


Geschwindigkeitssteigerung in PLINQ

Dieser Artikel enthält Informationen, mit denen Sie PLINQ-Abfragen schreiben können, die so effizient wie möglich sind und gleichzeitig korrekte Ergebnisse liefern.

Der Hauptzweck von PLINQ besteht darin, die Ausführung von LINQ to Objects-Abfragen zu beschleunigen, indem die Abfragedelegats parallel auf Multi-Core-Computern ausgeführt werden. PLINQ zeigt die beste Leistung, wenn die Verarbeitung der einzelnen Elemente in einer Quellsammlung unabhängig erfolgt, ohne gemeinsamen Zustand der einzelnen Delegaten. Solche Vorgänge sind in LINQ to Objects und PLINQ üblich und werden häufig als "wunderbar parallel" bezeichnet, da sie sich leicht für die Planung auf mehreren Threads eignen. Nicht alle Abfragen bestehen jedoch ganz aus reizvoll parallelen Vorgängen. In den meisten Fällen umfasst eine Abfrage einige Operatoren, die entweder nicht parallelisiert werden können oder die parallele Ausführung verlangsamen. Und auch bei Abfragen, die vollkommen parallel sind, muss PLINQ die Datenquelle weiterhin partitionieren und die Arbeit an den Threads planen und die Ergebnisse normalerweise zusammenführen, wenn die Abfrage abgeschlossen ist. All diese Vorgänge fügen zu den Rechenkosten der Parallelisierung hinzu; Diese Kosten für das Hinzufügen von Parallelisierung werden als Overhead bezeichnet. Um eine optimale Leistung in einer PLINQ-Abfrage zu erzielen, besteht das Ziel darin, die Teile zu maximieren, die wunderbar parallel sind, und die Teile zu minimieren, die Mehraufwand erfordern.

Faktoren, die sich auf die Leistung von PLINQ-Abfragen auswirken

In den folgenden Abschnitten werden einige der wichtigsten Faktoren aufgeführt, die sich auf die parallele Abfrageleistung auswirken. Dies sind allgemeine Aussagen, die allein nicht ausreichen, um die Abfrageleistung in allen Fällen vorherzusagen. Wie immer ist es wichtig, die tatsächliche Leistung bestimmter Abfragen auf Computern mit einer Reihe repräsentativer Konfigurationen und Lasten zu messen.

  1. Rechenkosten der Gesamtarbeit.

    Um die Beschleunigung zu erreichen, muss eine PLINQ-Abfrage zum Ausgleich des Mehraufwands in ausreichendem Maße optimal parallel verarbeitbar sein. Die Arbeit kann als Rechenaufwand der einzelnen Delegaten ausgedrückt werden, multipliziert mit der Anzahl der Elemente in der Quellauflistung. Angenommen, ein Vorgang kann parallelisiert werden, desto rechenintensiver ist es, desto größer ist die Möglichkeit zum Beschleunigen. Wenn eine Funktion beispielsweise eine Millisekunden zum Ausführen benötigt, dauert eine sequenzielle Abfrage über 1000 Elemente eine Sekunde, um diesen Vorgang auszuführen, während eine parallele Abfrage auf einem Computer mit vier Kernen nur 250 Millisekunden dauern kann. Dies führt zu einer Beschleunigung von 750 Millisekunden. Wenn die Ausführung der Funktion für jedes Element eine Sekunde beanspruchen würde, betrüge die Beschleunigung 750 Sekunden. Wenn der Delegat sehr aufwändig ist, könnte PLINQ mit nur wenigen Elementen in der Quellsammlung eine erhebliche Beschleunigung bieten. Im Gegensatz dazu eignen sich kleine Quellsammlungen mit trivialen Delegaten im Allgemeinen nicht gut für PLINQ.

    Im folgenden Beispiel ist queryA wahrscheinlich ein guter Kandidat für PLINQ, wobei davon ausgegangen wird, dass die Select-Funktion viel Arbeit erfordert. Wahrscheinlich ist queryB kein guter Kandidat, weil die Select-Funktion nicht mit genügend Arbeit verbunden ist, und der Mehraufwand der Parallelisierung den Vorteil der Beschleunigung mindestens überwiegend, wenn nicht vollständig aufhebt.

    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. Die Anzahl der logischen Kerne auf dem System (Grad der Parallelität).

    Dieser Punkt ist eine offensichtliche Folgerung aus dem vorherigen Abschnitt, denn Abfragen, die wunderbar parallel laufen, werden auf Computern mit mehr Kernen schneller ausgeführt, da die Arbeit unter mehr gleichzeitigen Threads aufgeteilt werden kann. Die Gesamtgeschwindigkeit hängt davon ab, welcher Prozentsatz der Gesamtarbeit der Abfrage parallelisierbar ist. Gehen Sie jedoch nicht davon aus, dass alle Abfragen doppelt so schnell auf einem acht Kerncomputer ausgeführt werden wie ein Vierkerncomputer. Bei der Optimierung von Abfragen zur optimalen Leistung ist es wichtig, die tatsächlichen Ergebnisse auf Computern mit verschiedenen Kernen zu messen. Dieser Punkt bezieht sich auf Punkt 1: Größere Datasets sind erforderlich, um größere Computerressourcen zu nutzen.

  3. Die Anzahl und Art von Vorgängen.

    PLINQ stellt den AsOrdered-Operator für Situationen bereit, in denen die Reihenfolge der Elemente in der Quellsequenz beibehalten werden muss. Es gibt Kosten für die Bestellung, aber diese sind normalerweise bescheiden. GroupBy- und Join-Vorgänge verursachen ebenfalls Mehraufwand. PLINQ führt am besten aus, wenn es erlaubt ist, Elemente in der Quellauflistung in beliebiger Reihenfolge zu verarbeiten und sie an den nächsten Operator zu übergeben, sobald sie bereit sind. Weitere Informationen finden Sie unter "Order Preservation" in PLINQ.

  4. Die Form der Abfrageausführung.

    Wenn Sie die Ergebnisse einer Abfrage speichern, indem Sie ToArray oder ToList aufrufen, müssen die Ergebnisse aller parallelen Threads mit der einzelnen Datenstruktur zusammengeführt werden. Dies ist mit unvermeidbarem Rechenaufwand verbunden. Ebenso müssen die Ergebnisse von den Arbeitsthreads auf den Enumeratorthread serialisiert werden, wenn Sie die Iteration über die Ergebnisse mithilfe einer foreach-Schleife („For Each“ in Visual Basic) durchführen. Wenn Sie jedoch nur eine Aktion basierend auf dem Ergebnis jedes Threads ausführen möchten, können Sie diese Arbeit mit der ForAll-Methode für mehrere Threads ausführen.

  5. Der Typ der Zusammenführungsoptionen.

    PLINQ kann entweder so konfiguriert werden, dass die Ausgabe gepuffert und in Blöcken oder alle gleichzeitig erzeugt wird, nachdem das gesamte Resultset erzeugt wurde, oder um einzelne Ergebnisse zu streamen, während sie erzeugt werden. Ersteres verringert die Gesamtausführungszeit, und Letzteres verringert die Latenz zwischen den zurückgegebenen Elementen. Die Zusammenführungsoptionen wirken sich zwar nicht immer auf die Gesamtleistung von Abfragen aus, können aber die wahrgenommene Leistung beeinträchtigen, da sie steuern, wie lange ein Benutzer warten muss, um Ergebnisse anzuzeigen. Weitere Informationen finden Sie unter "Zusammenführungsoptionen" in PLINQ.

  6. Die Art der Partitionierung.

    In einigen Fällen kann eine PLINQ-Abfrage über eine indizierbare Quellauflistung zu einer unausgewogenen Arbeitslast führen. In diesem Fall können Sie die Abfrageleistung erhöhen, indem Sie einen benutzerdefinierten Partitionierer erstellen. Weitere Informationen finden Sie unter Benutzerdefinierte Partitionierer für PLINQ und TPL.

Wenn PLINQ den sequenziellen Modus auswäht

PLINQ versucht immer, mindestens so schnell eine Abfrage auszuführen, wie die Abfrage sequenziell ausgeführt würde. Obwohl PLINQ nicht untersucht, wie rechenintensiv die Benutzerstellvertretungen sind oder wie groß die Eingabequelle ist, wird nach bestimmten Abfrage-"Shapes" gesucht. Insbesondere wird nach Abfrageoperatoren oder Kombinationen von Operatoren gesucht, die in der Regel dazu führen, dass eine Abfrage langsamer im Parallelmodus ausgeführt wird. Wenn diese Shapes gefunden werden, greift PLINQ standardmäßig auf den sequenziellen Modus zurück.

Nach der Messung der Leistung einer bestimmten Abfrage können Sie jedoch feststellen, dass sie tatsächlich schneller im parallelen Modus ausgeführt wird. In solchen Fällen können Sie das ParallelExecutionMode.ForceParallelism Flag über die WithExecutionMode Methode verwenden, um PLINQ anzuweisen, die Abfrage zu parallelisieren. Weitere Informationen finden Sie unter How to: Specify the Execution Mode in PLINQ.

In der folgenden Liste werden die Abfrage-Shapes beschrieben, die PLINQ standardmäßig im sequenziellen Modus ausführt:

  • Abfragen, die eine Select-, indizierte Where-, indizierte SelectMany- oder ElementAt-Klausel nach einem Sortierungs- oder Filterungsoperator enthalten, der ursprüngliche Indizes entfernt oder angeordnet hat.

  • Abfragen, die einen Take-, TakeWhile-, Skip- oder SkipWhile-Operator enthalten und bei denen die Indizes in der Quellsequenz nicht in der ursprünglichen Reihenfolge sind.

  • Abfragen, die Zip oder SequenceEquals enthalten, es sei denn, eine der Datenquellen enthält einen ursprünglich geordneten Index, und die andere Datenquelle ist indizierbar – d.h. Array oder IList(T).

  • Abfragen, die Concat enthalten, es sei denn, es wird auf indizierbare Datenquellen angewendet.

  • Abfragen, die Reverse enthalten, sofern nicht auf eine indizierbare Datenquelle angewendet.

Siehe auch