Przyspieszenie w PLINQ

Ten artykuł zawiera informacje ułatwiające pisanie zapytań PLINQ, które są tak wydajne, jak to możliwe, przy jednoczesnym zachowaniu prawidłowych wyników.

Głównym celem PLINQ jest przyspieszenie wykonywania zapytań LINQ to Objects przez równoległe wykonywanie delegatów zapytań na komputerach wielordzeniowych. PlINQ działa najlepiej, gdy przetwarzanie każdego elementu w kolekcji źródłowej jest niezależne, bez współużytkowanego stanu między poszczególnymi delegatami. Takie operacje są powszechne w LINQ to Objects i PLINQ i są często nazywane "uroczo równoległe", ponieważ łatwo nadają się do planowania na wielu wątkach. Jednak nie wszystkie zapytania składają się całkowicie z wspaniałych operacji równoległych. W większości przypadków zapytanie obejmuje niektóre operatory, które nie mogą być zrównane lub spowalniają wykonywanie równoległe. A nawet w przypadku zapytań, które są całkowicie równoległe, PLINQ musi nadal partycjonować źródło danych i zaplanować pracę nad wątkami, a zwykle scalić wyniki po zakończeniu zapytania. Wszystkie te operacje dodają do kosztów obliczeniowych równoległości; koszty dodawania równoległości są nazywane narzutami. Aby osiągnąć optymalną wydajność w zapytaniu PLINQ, celem jest zmaksymalizowanie części, które są uroczo równoległe i zminimalizować części, które wymagają narzutu.

Czynniki wpływające na wydajność zapytań PLINQ

W poniższych sekcjach wymieniono niektóre z najważniejszych czynników wpływających na wydajność zapytań równoległych. Są to ogólne instrukcje, które same w sobie nie są wystarczające do przewidywania wydajności zapytań we wszystkich przypadkach. Jak zawsze ważne jest mierzenie rzeczywistej wydajności określonych zapytań na komputerach z szeregiem reprezentatywnych konfiguracji i obciążeń.

  1. Koszt obliczeniowy ogólnej pracy.

    Aby osiągnąć szybkość, zapytanie PLINQ musi mieć wystarczającą liczbę wspaniałych równoległych prac, aby zrównoważyć obciążenie. Praca może być wyrażona jako koszt obliczeniowy każdego delegata pomnożony przez liczbę elementów w kolekcji źródłowej. Przy założeniu, że operacja może być zrównana, tym bardziej kosztowna jest operacja, tym większa szansa na przyspieszenie. Jeśli na przykład funkcja przyjmuje jedną milisekundę do wykonania, zapytanie sekwencyjne obejmujące ponad 1000 elementów zajmie jedną sekundę, aby wykonać tę operację, natomiast zapytanie równoległe na komputerze z czterema rdzeniami może potrwać tylko 250 milisekund. Daje to przyspieszenie 750 milisekund. Jeśli funkcja wymagała jednej sekundy do wykonania dla każdego elementu, szybkość będzie 750 sekund. Jeśli delegat jest bardzo kosztowny, to PLINQ może oferować znaczne przyspieszenie tylko z kilkoma elementami w kolekcji źródłowej. Z drugiej strony małe kolekcje źródłowe z trywialnymi delegatami zazwyczaj nie są dobrymi kandydatami do PLINQ.

    W poniższym przykładzie zapytanieA jest prawdopodobnie dobrym kandydatem dla PLINQ, zakładając, że jego funkcja Select obejmuje dużo pracy. zapytanieB prawdopodobnie nie jest dobrym kandydatem, ponieważ nie ma wystarczającej ilości pracy w instrukcji Select, a obciążenie związane z równoległością zrównoważy większość lub wszystkie przyspieszenie.

    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. Liczba rdzeni logicznych w systemie (stopień równoległości).

    Jest to oczywista współpraca z poprzednią sekcją, zapytania, które są niezwykle równoległe uruchamiane szybciej na maszynach z większą większa większa liczba rdzeni, ponieważ praca może być podzielona między bardziej współbieżne wątki. Ogólna ilość przyspieszenia zależy od tego, jaki procent ogólnej pracy zapytania można zrównać. Nie należy jednak zakładać, że wszystkie zapytania będą działać dwa razy szybciej na ośmiordzeniowym komputerze jako czterordzeniowy komputer. Podczas dostrajania zapytań w celu uzyskania optymalnej wydajności ważne jest mierzenie rzeczywistych wyników na komputerach z różnymi liczbami rdzeni. Ten punkt jest związany z punktem 1: większe zestawy danych są wymagane do korzystania z większych zasobów obliczeniowych.

  3. Liczba i rodzaj operacji.

    PLINQ udostępnia operator AsOrdered w sytuacjach, w których konieczne jest zachowanie kolejności elementów w sekwencji źródłowej. Istnieje koszt związany z zamawianiem, ale koszt ten jest zwykle skromny. Operacje grupowania i dołączania również powodują narzut. PlINQ działa najlepiej, gdy może przetwarzać elementy w kolekcji źródłowej w dowolnej kolejności i przekazywać je do następnego operatora, gdy tylko będą gotowe. Aby uzyskać więcej informacji, zobacz Zachowywanie kolejności w PLINQ.

  4. Forma wykonywania zapytania.

    Jeśli przechowujesz wyniki zapytania przez wywołanie metody ToArray lub ToList, wyniki ze wszystkich równoległych wątków muszą zostać scalone w jedną strukturę danych. Obejmuje to nieunikniony koszt obliczeniowy. Podobnie, jeśli iterujesz wyniki przy użyciu pętli foreach (For Each in Visual Basic), wyniki wątków roboczych muszą być serializowane na wątku wyliczającego. Jeśli jednak chcesz wykonać jakąś akcję na podstawie wyniku z każdego wątku, możesz użyć metody ForAll, aby wykonać tę pracę na wielu wątkach.

  5. Typ opcji scalania.

    PlINQ można skonfigurować tak, aby buforować jego dane wyjściowe i produkować je we fragmentach lub wszystkie naraz po utworzeniu całego zestawu wyników lub w celu przesyłania strumieniowego pojedynczych wyników w miarę ich produkcji. Pierwsze wyniki zmniejszyły całkowity czas wykonywania, a drugie powoduje zmniejszenie opóźnienia między zwracanych elementów. Chociaż opcje scalania nie zawsze mają duży wpływ na ogólną wydajność zapytań, mogą mieć wpływ na postrzeganą wydajność, ponieważ kontrolują, jak długo użytkownik musi czekać, aby zobaczyć wyniki. Aby uzyskać więcej informacji, zobacz Opcje scalania w PLINQ.

  6. Rodzaj partycjonowania.

    W niektórych przypadkach zapytanie PLINQ dotyczące kolekcji źródłowej z możliwością indeksowania może spowodować niezrównoważone obciążenie pracą. W takim przypadku może być możliwe zwiększenie wydajności zapytań przez utworzenie niestandardowego partycjonatora. Aby uzyskać więcej informacji, zobacz Custom Partitioners for PLINQ and TPL (Niestandardowe partycjonatory dla PLINQ i TPL).

Gdy plINQ wybiera tryb sekwencyjny

PlINQ zawsze będzie podejmować próby wykonania zapytania co najmniej tak szybko, jak zapytanie będzie uruchamiane sekwencyjnie. Chociaż PLINQ nie patrzy na to, jak obliczenia kosztują delegaty użytkownika lub jak duże jest źródło danych wejściowych, szuka pewnych zapytań "kształtów". W szczególności szuka operatorów zapytań lub kombinacji operatorów, które zwykle powodują, że zapytanie działa wolniej w trybie równoległym. Po znalezieniu takich kształtów funkcja PLINQ domyślnie wraca do trybu sekwencyjnego.

Jednak po zmierzeniu wydajności określonego zapytania można określić, że faktycznie działa szybciej w trybie równoległym. W takich przypadkach można użyć flagi ParallelExecutionMode.ForceParallelism za pośrednictwem WithExecutionMode metody , aby poinstruować PLINQ, aby zrównoleglić zapytanie. Aby uzyskać więcej informacji, zobacz How to: Specify the Execution Mode in PLINQ (Instrukcje: określanie trybu wykonywania w plINQ).

Na poniższej liście opisano kształty zapytania, które domyślnie będą wykonywane w trybie sekwencyjnym:

  • Zapytania zawierające klauzulę Select, indexed Where, indexed SelectMany lub ElementAt po operatorze porządkowania lub filtrowania, który usunął lub przemieł oryginalne indeksy.

  • Zapytania zawierające operator Take, TakeWhile, Skip, SkipWhile i gdzie indeksy w sekwencji źródłowej nie są w oryginalnej kolejności.

  • Zapytania zawierające plik Zip lub SequenceEquals, chyba że jedno ze źródeł danych ma pierwotnie uporządkowany indeks, a inne źródło danych można indeksować (tj. tablicę lub IList(T)).

  • Zapytania zawierające concat, chyba że są stosowane do źródeł danych z możliwością indeksowania.

  • Zapytania zawierające odwrotne, chyba że zastosowano je do indeksowalnego źródła danych.

Zobacz też