To mnie zastanawia, jak ważna jest wielowątkowość w obecnym scenariuszu branżowym?
W obszarach krytycznych pod względem wydajności, w których wydajność nie pochodzi od kodu innej firmy wykonującego ciężkie prace, ale naszego własnego, to zwykle rozważam rzeczy w tej kolejności ważności z punktu widzenia procesora (GPU to symbol wieloznaczny, który wygrałem nie wchodzi):
- Wydajność pamięci (np .: lokalizacja odniesienia).
- Algorytmiczne
- Wielowątkowość
- SIMD
- Inne optymalizacje (wskazówki dotyczące przewidywania gałęzi statycznych, np.)
Należy pamiętać, że ta lista nie opiera się wyłącznie na znaczeniu, ale na wielu innych dynamikach, takich jak wpływ, jaki mają one na utrzymanie, jak proste są (jeśli nie, warto rozważyć je wcześniej), ich interakcje z innymi na liście itp.
Wydajność pamięci
Najbardziej może dziwić mój wybór wydajności pamięci w porównaniu z algorytmem. Wynika to z faktu, że wydajność pamięci współdziała ze wszystkimi 4 innymi pozycjami na tej liście, i dlatego, że rozpatrywanie jej często dotyczy kategorii „projektowanie”, a nie „implementacji”. Jest co prawda trochę problemu z kurczakiem lub jajkiem, ponieważ zrozumienie wydajności pamięci często wymaga uwzględnienia wszystkich 4 pozycji na liście, podczas gdy wszystkie 4 inne pozycje również wymagają rozważenia wydajności pamięci. Ale to jest sedno wszystkiego.
Na przykład, jeśli potrzebujemy struktury danych, która oferuje sekwencyjny dostęp w czasie liniowym i wstawianie w czasie stałym z tyłu i nic więcej dla małych elementów, naiwnym wyborem, do którego należy sięgnąć, byłaby połączona lista. Pomija to wydajność pamięci. Jeśli weźmiemy pod uwagę wydajność pamięci w miksie, to ostatecznie wybieramy bardziej ciągłe struktury w tym scenariuszu, takie jak rozwijalne struktury oparte na macierzy lub bardziej ciągłe węzły (np. Jeden przechowujący 128 elementów w węźle) połączone ze sobą, a przynajmniej połączona lista wspierana przez alokator puli. Mają one dramatyczną przewagę, mimo że mają tę samą złożoność algorytmiczną. Podobnie często wybieramy szybkie sortowanie tablicy zamiast sortowania scalonego pomimo mniejszej złożoności algorytmicznej po prostu ze względu na wydajność pamięci.
Podobnie, nie możemy mieć wydajnego wielowątkowości, jeśli nasze wzorce dostępu do pamięci są tak szczegółowe i rozproszone, że w rezultacie maksymalizujemy ilość fałszywego udostępniania, jednocześnie blokując na najbardziej szczegółowych poziomach w kodzie. Tak więc wydajność pamięci zwielokrotnia wydajność wielowątkowości. Jest to warunek wstępny, aby jak najlepiej wykorzystać wątki.
Każda pozycja powyżej na liście ma złożoną interakcję z danymi, a skupienie się na tym, jak dane są reprezentowane, zależy ostatecznie od wydajności pamięci. Każde z powyższych może być utrudnione przez niewłaściwy sposób reprezentowania lub dostępu do danych.
Innym powodem jest to, sprawność pamięci, więc ważne jest, że można go zastosować w całej całym kodzie. Zasadniczo, gdy ludzie wyobrażają sobie, że nieefektywność kumuluje się z niewielkimi fragmentami pracy tu i tam, jest to znak, że muszą pobrać profiler. Jednak pola o niskim opóźnieniu lub te, które mają do czynienia z bardzo ograniczonym sprzętem, w rzeczywistości znajdą, nawet po profilowaniu, sesje, które wskazują brak wyraźnych punktów aktywnych (tylko czasami rozproszone po całym miejscu) w bazie kodu, która jest rażąco nieefektywna w sposobie przydzielania, kopiowania i dostęp do pamięci. Zazwyczaj jest to jedyny raz, kiedy cała baza kodu może być podatna na problemy z wydajnością, które mogą prowadzić do zastosowania zupełnie nowego zestawu standardów w całej bazie kodu, a wydajność pamięci jest często jej istotą.
Algorytmiczne
Ten jest dość oczywisty, ponieważ wybór w algorytmie sortowania może zrobić różnicę między ogromnym wejściem zajmującym miesiące sortowania a sekundami sortowania. Ma największy wpływ ze wszystkich, jeśli wybór jest między, powiedzmy, naprawdę słabo rozwiniętymi algorytmami kwadratowymi lub sześciennymi a algorytmem liniowo-rytmicznym, lub między liniowym a logarytmicznym lub stałym, przynajmniej do czasu, aż uzyskamy około 1 000 000 podstawowych maszyn (w tym przypadku pamięć wydajność stałaby się jeszcze ważniejsza).
Nie znajduje się jednak na szczycie mojej osobistej listy, ponieważ każdy kompetentny w swojej dziedzinie znałby strukturę przyspieszania w celu eliminacji frustum, np. Jesteśmy nasyceni wiedzą algorytmiczną i znamy takie rzeczy, jak używanie wariantu trie, takiego jak drzewo radix dla wyszukiwań opartych na prefiksach to rzeczy dla dzieci. Brak takiej podstawowej wiedzy na temat dziedziny, w której pracujemy, sprawiłby, że wydajność algorytmiczna z pewnością wzniosłaby się na szczyt, ale często wydajność algorytmiczna jest banalna.
Również wynalezienie nowych algorytmów może być koniecznością w niektórych dziedzinach (np. W przetwarzaniu siatki musiałem wymyślić setki, ponieważ albo wcześniej nie istniały, albo implementacje podobnych funkcji w innych produktach były zastrzeżonymi tajemnicami, nie opublikowanymi w pracy ). Jednak gdy miniemy część dotyczącą rozwiązywania problemów i znajdziemy sposób na uzyskanie prawidłowych wyników, a gdy efektywność stanie się celem, jedynym sposobem na osiągnięcie tego jest zastanowienie się, w jaki sposób wchodzimy w interakcję z danymi (pamięcią). Bez zrozumienia wydajności pamięci nowy algorytm może stać się niepotrzebnie złożony dzięki daremnym wysiłkom, aby przyspieszyć go, gdy jedyną rzeczą, której potrzebował, było nieco większe rozważenie wydajności pamięci w celu uzyskania prostszego, bardziej eleganckiego algorytmu.
Wreszcie, algorytmy są bardziej w kategorii „implementacja” niż wydajność pamięci. Często są łatwiejsze do poprawienia z perspektywy czasu, nawet przy początkowo nieoptymalnym algorytmie. Na przykład gorszy algorytm przetwarzania obrazu jest często implementowany tylko w jednym lokalnym miejscu w bazie kodu. Później można go wymienić na lepszy. Jeśli jednak wszystkie algorytmy przetwarzania obrazu są powiązane z Pixel
interfejsem, który ma nieoptymalną reprezentację pamięci, ale jedynym sposobem na poprawienie tego jest zmiana sposobu reprezentacji wielu pikseli (a nie jednego), wtedy często jesteśmy SOL i będzie musiał całkowicie przepisać bazę kodu w kierunkuImage
berło. To samo dotyczy zastąpienia algorytmu sortowania - zwykle jest to szczegół implementacji, podczas gdy pełna zmiana podstawowej reprezentacji sortowanych danych lub sposobu ich przesyłania przez wiadomości może wymagać przeprojektowania interfejsów.
Wielowątkowość
Wielowątkowość jest trudna w kontekście wydajności, ponieważ jest to optymalizacja na poziomie mikro, grająca według cech sprzętowych, ale nasz sprzęt naprawdę skaluje się w tym kierunku. Już mam rówieśników, którzy mają 32 rdzenie (mam tylko 4).
Jednak wielowątkowość jest jedną z najniebezpieczniejszych mikrooptymalizacji prawdopodobnie znanych profesjonalistom, jeśli celem jest przyspieszenie oprogramowania. Stan wyścigu jest najbardziej zabójczym możliwym błędem, ponieważ ma tak nieokreślony charakter (być może pojawia się tylko raz na kilka miesięcy na maszynie programisty w najbardziej niewygodnym momencie poza kontekstem debugowania, jeśli w ogóle). Ma więc prawdopodobnie najbardziej negatywny wpływ na łatwość konserwacji i potencjalną poprawność kodu spośród nich wszystkich, zwłaszcza, że błędy związane z wielowątkowością mogą łatwo latać pod radarem nawet najbardziej dokładnych testów.
Niemniej jednak staje się to bardzo ważne. Chociaż wciąż nie zawsze może to przebijać wydajność pamięci (co czasami może przyspieszać sto razy szybciej), biorąc pod uwagę liczbę rdzeni, które mamy teraz, widzimy coraz więcej rdzeni. Oczywiście nawet w przypadku 100-rdzeniowych maszyn nadal umieszczam wydajność pamięci na szczycie listy, ponieważ bez niej wydajność wątków jest na ogół niemożliwa. Program może korzystać ze stu wątków na takiej maszynie i nadal jest powolny bez wydajnej reprezentacji pamięci i wzorców dostępu (które wiążą się z wzorcami blokującymi).
SIMD
SIMD jest również trochę niewygodny, ponieważ rejestry stają się coraz szersze, a plany są jeszcze szersze. Początkowo widzieliśmy 64-bitowe rejestry MMX, a następnie 128-bitowe rejestry XMM zdolne do 4 równoległych operacji SPFP. Teraz widzimy 256-bitowe rejestry YMM zdolne do 8 równolegle. Istnieją już plany dotyczące rejestrów 512-bitowych, które pozwoliłyby na 16 równolegle.
Będą one oddziaływać i rozmnażać się przy wydajności wielowątkowości. Jednak SIMD może obniżyć łatwość konserwacji tak samo, jak wielowątkowość. Chociaż związane z nimi błędy niekoniecznie są tak trudne do odtworzenia i naprawienia jak impas lub warunki wyścigu, przenośność jest niewygodna, a zapewnienie, że kod może działać na wszystkich komputerach (i przy użyciu odpowiednich instrukcji opartych na ich możliwościach sprzętowych) jest niezręczny.
Inną rzeczą jest to, że chociaż dzisiejsze kompilatory zwykle nie pobijają fachowo napisanego kodu SIMD, łatwo pokonują naiwne próby. Mogą poprawić się do tego stopnia, że nie musimy już robić tego ręcznie, a przynajmniej nie stać się tak ręcznym, aby pisać wewnętrzne instrukcje lub prosty kod asemblera (być może po prostu trochę ludzkiego przewodnika).
Ponownie jednak bez układu pamięci wydajnego do przetwarzania wektoryzacyjnego karta SIMD jest bezużyteczna. W końcu załadujemy jedno pole skalarne do szerokiego rejestru, aby wykonać tylko jedną operację. Istotą wszystkich tych elementów jest zależność od układów pamięci, aby była naprawdę wydajna.
Inne optymalizacje
Często sugeruję, abyśmy zaczęli nazywać je teraz „mikro”, jeśli słowo to sugeruje nie tylko wyjście poza skupienie się na algorytmie, ale także na zmiany, które mają niewielki wpływ na wydajność.
Często próba optymalizacji pod kątem przewidywania gałęzi wymaga zmiany algorytmu lub wydajności pamięci, np. Jeśli spróbuje się tego jedynie poprzez podpowiedzi i przestawienie kodu w celu przewidywania statycznego, poprawia to tylko wykonanie pierwszego kodu po raz pierwszy, co powoduje, że efekty są wątpliwe, jeśli często nieistotny.
Powrót do wielowątkowości dla wydajności
W każdym razie, jak ważna jest wielowątkowość w kontekście wydajności? Na mojej 4-rdzeniowej maszynie idealnie może zrobić to około 5 razy szybciej (co mogę uzyskać dzięki hyperthreading). Byłoby to znacznie ważniejsze dla mojego kolegi, który ma 32 rdzenie. I będzie coraz ważniejsze w nadchodzących latach.
To bardzo ważne. Ale nie ma sensu po prostu rzucać wiązką wątków na problem, jeśli nie ma wydajności pamięci, aby pozwolić na oszczędne używanie blokad, aby zmniejszyć fałszywe udostępnianie itp.
Wielowątkowość poza wydajnością
Wielowątkowość nie zawsze polega na czystej wydajności w sensie bezpośredniej przepustowości. Czasami służy do równoważenia obciążenia nawet przy możliwym koszcie przepustowości, aby poprawić reakcję na użytkownika lub pozwolić użytkownikowi wykonać więcej zadań wielozadaniowych bez czekania na zakończenie (np. Kontynuuj przeglądanie podczas pobierania pliku).
W takich przypadkach sugerowałbym, że wielowątkowość rośnie jeszcze wyżej w górę (być może nawet powyżej wydajności pamięci), ponieważ chodzi tu raczej o projektowanie użytkownika niż o maksymalne wykorzystanie sprzętu. Często zdominuje projekty interfejsów i sposób, w jaki budujemy całą naszą bazę kodu w takich scenariuszach.
Kiedy nie ograniczamy się jedynie do zacieśnienia pętli dostępu do ogromnej struktury danych, wielowątkowość przechodzi do naprawdę ostrej kategorii „projektowanie”, a projektowanie zawsze przebija implementację.
Tak więc w tych przypadkach powiedziałbym, że rozważenie wielowątkowości z góry jest absolutnie niezbędne, nawet bardziej niż reprezentacja pamięci i dostęp.