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
Przełączanie kontekstu to około 5000 do 10000 cykli procesora.
- Synchronizacja danych
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
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
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ść
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