czwartek, 10 lipca 2014

Wielowątkowość - wydajność i skalowalność

Skalowalność  i wydajność



Skalowalność określa możliwość aby zwiększyć przepustowość lub wielkość wykonywanej pracy kiedy dostępne są dodatkowe zasoby takie jak procesor, pamięć, przepustowość I/O.

Należy pamiętać aby najpierw robić aplikacje która działa prawidłowo a następnie wykonywać optymalizacje i tylko jeśli jest ona potrzebna czyli aplikacja nie spełnia oczekiwań.

Prawo Amdahl'a

 

Cześć pracy można wykonać szybciej kiedy dostępnych jest więcej zasobów np zbieranie truskawek, natomiast część nie dlatego że jakaś cześć zadania może być wykonana tylko przez jeden wątek np rodzenie dzieci (9 kobiet nie urodzi 1 dziecka w miesiąc).
Większość wielowątkowych programów ma właśnie takie części które może wykonać wiele wątków i takie które mogą być wykonane tylko przez jeden wątek. Prawo Amdahla określa maksymalne przyspieszenie przy znanej części serializowalnej (jednowątkowej) i liczbie procesorów w stosunku do początkowej liczby procesorów.

Speedup <= 1/ (F + (1-F)/N)

F - ułamek pracy wykonywanej jednowątkowo
N - liczba procesorów

Przykładowo jeśli program nie ma części jednowątkowych (co jest niemożliwe) to przyspieszenie wyniesie 1/(0 + 1/N) = N czyli przy 5 procesorach będzie działać 5 razy szybciej.
Jeśli połowa programu jest wykonana jednowątkowo (F=0.5) to przy 5 procesorach przyspieszenie nie będzie większe niż = 1/ (0.5 + 0,5/5) = 1.67 czyli nie zwięszy się nawet dwukrotnie, tak naprawdę przyspieszenie będzie dążyć tutaj do 2.
Jeśli zwiększymy liczbę procesorów do nieskończoności to wartość przyspieszenia nie będzie większa niż 1/F.

To sprawia jak wielkie znaczenie ma cześć programu która może być wykonana przez jeden wątek dla skalowalności.
Nawet jeśli w naszej aplikacji mamy perfekcyjnie niezależne zadania to muszą one być pobierane np z kolejki i wyniki też najpewniej muszą być zapisywane więc nawet w takim idealnym przypadku część pracy jest wykonywana tylko przez jeden wątek.

Koszty związane z pracą z wieloma wątkami

 

  • Context switching - przeładowanie kontekstu
Jeśli liczba wątków jest większa niż liczba procesorów to muszą one działać naprzemiennie. Podczas zawieszania wątku i zaczynania innego następuje przeładowanie kontekstu. Nie jest to darmowa operacja ponieważ wymaga manipulowania strukturami danych w systemie operacyjnym i JVM. Po starcie nowego wątku jego dane nie będą w cachu procesora więc bedzię on działał na początku wolniej. To powoduje że każdy wątek bez względu ile czeka innych wątków dostaje jakiś minimalny czas pracy. Jeśli wątek zostaje zablokowany poprzez blokadę lub IO to może on być przełączony szybciej niż jego minimalny czas. Programy z dużą liczbą blokad i operacji IO mają dużo operacji przełączania kontekstu.
Przełączanie kontekstu to około 5000 do 10000 cykli procesora.

  • Synchronizacja danych
Synchronizowane dane poprzez blokady lub słowo kluczowe violatile powoduje to że nie mogą być wykonywane pewne optymalizacje ponieważ należy zagwarantować widzialność danych po ich zapisaniu. To pociąga też za sobą używanie specjalnych instrukcji nazywanych memory barriers.
Mogą one inwalidować cache co powoduje dodatkowe zwiększenie kosztów.
W przypadku synchronizacji należy rozróżnić blokady obciążone (contended) od nieobciążonych (uncontended). Np synchronizacja violatile jest nieobciążona i nie jest bardzo kosztowna (20-250 cykli procesora). Kompilator może także optymalizować wielokrotne blokady w jedną a także określić że blokada jest niepotrzebna bo dana jest niedostępna dla innych wątków np. vector który używany jest w metodzie (czyli dostępny jest na stosie nie stercie).

  • Bloki synchronizowane
Blokowanie które jest obciążone (contended) powoduje że wątek jest albo wstrzymywany i czeka na to aż inny wątek zwolni blokadę i go wybudzi, albo powtarza próbę dostępu do blokady (spin-waiting).
Pierwsze rozwiązanie jest dużo bardziej efektywne ale jeśli blokady są trzymane bardzo któtko to JVM może się przełączyć na pewnych z nich na spin-waiting. 
Wstrzymywanie wątku pociąga za sobą 2 przełączenia wątku.


Redukowanie obciążenia blokad


Jak widać wyżej - blokady powodują że cześć programu jest wykonywana tylko przez jeden wątek.
Obciążenie blokad może znacząco ogranicza skalowalność systemu.
Aby więc zredukować obciążenie należy:
  • zredukować czas trzymania blokady
  • zredukować częstotliwość używania blokad
  • zastąpić blokady innymi mechanizmami które zwiększą współbieżność
Należy pamiętać aby blokada była trzymana tak krótko jak tylko potrzeba - wszystkie niepotrzebne operacje powinny być wyjęte z bloku synchronizowanego.

Innym przykładem zredukowania obciążenia blokady jest jej podzielenie (lock striping) co jest np wykorzystywane w klasie ConcurentHashMap która ma 16 blokad każda obsługuje N%16 kubełek z danymi - to powoduje znaczną poprawę współbieżności.
Należy pamiętać aby ograniczyć używanie takzwanych gorących pól - np liczników itp które muszą być zmieniane przy każdej operacji - lub używać do nich zmiennych atomowych (AtomicInteger, AtomicReference itp).
Alternatywą mogą być jak już wspomniano zmienne atomowe lub blokady ReadWrite które mogę zwiększyć współbieżność jeśli większość operacji jest tylko do odczytu.


Brak komentarzy:

Prześlij komentarz