Zrychlení v PLINQ
Tento článek obsahuje informace, které vám pomůžou psát dotazy PLINQ, které jsou co nejefektivnější a zároveň poskytují správné výsledky.
Primárním účelem PLINQ je zrychlit provádění dotazů LINQ to Objects spuštěním delegátů dotazů paralelně na počítačích s více jádry. PLINQ funguje nejlépe, když je zpracování každého prvku ve zdrojové kolekci nezávislé, bez sdíleného stavu zapojeného mezi jednotlivé delegáty. Tyto operace jsou běžné v LINQ to Objects a PLINQ, a často se nazývají "nádherně paralelní", protože se snadno plánují na více vláken. Ne všechny dotazy se ale skládají výhradně z nádherných paralelních operací. Ve většině případů dotaz zahrnuje některé operátory, které nelze paralelizovat nebo které zpomalují paralelní spouštění. A dokonce i s dotazy, které jsou zcela nádherně paralelní, PLINQ musí stále rozdělit zdroj dat a naplánovat práci na vláknech a obvykle sloučit výsledky po dokončení dotazu. Všechny tyto operace se přidávají do výpočetních nákladů paralelizace; tyto náklady na přidání paralelizace se nazývají režijní náklady. Chcete-li dosáhnout optimálního výkonu v dotazu PLINQ, je cílem maximalizovat části, které jsou příjemně paralelní a minimalizovat části, které vyžadují režii.
Faktory ovlivňující výkon dotazů PLINQ
Následující části obsahují některé z nejdůležitějších faktorů, které mají vliv na výkon paralelních dotazů. Jedná se o obecné příkazy, které samy o sobě nestačí k predikci výkonu dotazů ve všech případech. Stejně jako vždy je důležité měřit skutečný výkon konkrétních dotazů na počítačích s řadou reprezentativních konfigurací a zatížení.
Výpočetní náklady na celkovou práci
Aby bylo dosaženo zrychlení, musí mít dotaz PLINQ dostatečně příjemnou paralelní práci, aby se odsadily režijní náklady. Práce může být vyjádřena jako výpočetní náklady každého delegáta vynásobené počtem prvků ve zdrojové kolekci. Za předpokladu, že operaci lze paralelizovat, tím více výpočetně nákladnější je, tím větší je možnost zrychlení. Pokud například funkce trvá spuštění jednoho milisekundy, bude sekvenční dotaz více než 1 000 prvků trvat jednu sekundu k provedení této operace, zatímco paralelní dotaz na počítači se čtyřmi jádry může trvat pouze 250 milisekund. Tím se urychlí 750 milisekund. Pokud funkce vyžadovala provedení jedné sekundy pro každý prvek, zrychlení by bylo 750 sekund. Pokud je delegát velmi nákladný, může PLINQ nabídnout výrazné zrychlení pouze s několika položkami ve zdrojové kolekci. Naopak malé zdrojové kolekce s triviálními delegáty nejsou obecně vhodnými kandidáty pro PLINQ.
V následujícím příkladu je dotazA pravděpodobně vhodným kandidátem pro PLINQ za předpokladu, že jeho funkce Select zahrnuje hodně práce. dotazB pravděpodobně není vhodným kandidátem, protože v příkazu Select není dostatek práce a režie paralelizace posune většinu nebo všechny zrychlení.
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
Počet logických jader v systému (stupeň paralelismu).
Tento bod je zřejmým zápisem do předchozí části, dotazy, které jsou příjemně paralelní, běží na počítačích s více jádry rychleji, protože práci je možné rozdělit mezi více souběžných vláken. Celkové množství zrychlení závisí na tom, jaké procento celkové práce dotazu je paralelizovatelné. Nepředpokládáme však, že všechny dotazy budou běžet dvakrát rychleji na osmi jádrovém počítači jako na čtyřjádrový počítač. Při ladění dotazů pro optimální výkon je důležité měřit skutečné výsledky na počítačích s různými čísly jader. Tento bod souvisí s bodem č. 1: k využití větších výpočetních prostředků se vyžadují větší datové sady.
Počet a druh operací.
PLINQ poskytuje operátor AsOrdered pro situace, kdy je nutné zachovat pořadí prvků ve zdrojové sekvenci. Existují náklady spojené s objednáním, ale tyto náklady jsou obvykle skromné. Operace GroupBy a Join také účtují režijní náklady. PLINQ funguje nejlépe, když je povoleno zpracovávat prvky ve zdrojové kolekci v libovolném pořadí a předat je dalšímu operátoru, jakmile jsou připravené. Další informace naleznete v tématu Zachování objednávek v PLINQ.
Forma provádění dotazu.
Pokud ukládáte výsledky dotazu voláním ToArray nebo ToList, výsledky ze všech paralelních vláken musí být sloučeny do jedné datové struktury. To zahrnuje neuložené výpočetní náklady. Podobně pokud iterujete výsledky pomocí smyčky foreach (For Each v jazyce Visual Basic), výsledky z pracovních vláken musí být serializovány na vlákno enumerátoru. Pokud ale chcete pouze provést nějakou akci na základě výsledku z každého vlákna, můžete k provedení této práce použít ForAll metodu na více vláknech.
Typ možností sloučení.
PLINQ lze nakonfigurovat tak, aby buď ukládá výstup do vyrovnávací paměti, a vytvořil ho v blocích, nebo všechny najednou po vytvoření celé sady výsledků, nebo jinak aby streamoval jednotlivé výsledky při jejich vytváření. Výsledkem prvního je snížení celkové doby provádění a výsledkem druhé je snížení latence mezi výnosnými prvky. I když možnosti sloučení nemají vždy velký dopad na celkový výkon dotazů, můžou mít vliv na výkon, protože řídí, jak dlouho musí uživatel čekat na zobrazení výsledků. Další informace naleznete v tématu Možnosti sloučení v PLINQ.
Druh dělení.
V některých případech může dotaz PLINQ nad indexovatelnou zdrojovou kolekcí způsobit nevyvážené pracovní zatížení. Pokud k tomu dojde, můžete zvýšit výkon dotazu vytvořením vlastního dělitele. Další informace naleznete v tématu Vlastní partitionery pro PLINQ a TPL.
Když PLINQ zvolí sekvenční režim
PLINQ se vždy pokusí spustit dotaz alespoň tak rychle, jak by dotaz běžel postupně. Přestože PLINQ nevyhlíží na výpočetně nákladné delegáty uživatelů nebo jak velký je vstupní zdroj, hledá určité "obrazce dotazu". Konkrétně hledá operátory dotazů nebo kombinace operátorů, které obvykle způsobují pomalejší spouštění dotazu v paralelním režimu. Když takové obrazce najde, plINQ se ve výchozím nastavení vrátí do sekvenčního režimu.
Po měření výkonu konkrétního dotazu ale můžete zjistit, že ve skutečnosti běží rychleji v paralelním režimu. V takových případech můžete příznakem ParallelExecutionMode.ForceParallelism prostřednictvím WithExecutionMode metody instruovat PLINQ k paralelizaci dotazu. Další informace naleznete v tématu Postupy: Určení režimu spouštění v PLINQ.
Následující seznam popisuje obrazce dotazu, které plINQ ve výchozím nastavení spustí v sekvenčním režimu:
Dotazy, které obsahují klauzuli Select, indexed Where, indexed SelectMany nebo ElementAt za operátorem řazení nebo filtrování, který odebral nebo změnil uspořádání původních indexů.
Dotazy obsahující operátor Take, Take While, Skip, Skip While a kde indexy ve zdrojové sekvenci nejsou v původním pořadí.
Dotazy obsahující zip nebo SequenceEquals, pokud některý ze zdrojů dat nemá původně seřazený index a druhý zdroj dat je indexovatelný (tj. pole nebo IList(T)).
Dotazy, které obsahují Concat, pokud nejsou použity pro indexovatelné zdroje dat.
Dotazy, které obsahují reverse, pokud nejsou použity u indexovatelného zdroje dat.