Wielowątkowość a wydajność.

Wielowątkowość a wydajność.
MA
  • Rejestracja:prawie 8 lat
  • Ostatnio:prawie 5 lat
  • Postów:25
0

Mam pytanie na które nie mogę znaleźć odpowiedzi, mianowicie jak obciążające dla procesorów jest "przepinanie" się z wykonywania wątku na inny wątek. Aby zostać dobrze zrozumianym postaram się przybliżyć problem podając przykład(procesor 1 rdzeniowy), jest aplikacja a i aplikacja b. Aplikacja a posiada 10000 wątków(wątki powołane są na początku, tzn. nie kaskadowo podczas działania programu), każdy wątek wykonuje operacje dodania jedynki do statycznej zmiennej i rozpoczyna działanie kolejnego wątku. Aplikacja b ma jedynie 1 wątek i wartości dodaje w pętli aż zmienna osiągnie wartość 10000. Zakładam, że aplikacja b wykona się szybciej bo procesor nie będzie przepinał się z wątku na wątek(proszę o pominięcie obciążenia wynikającego z powoływania samych wątków w programie itd., tylko obciązenie wynikające z "przepięcia"). Jak dużym wysiłkiem jest operacja którą opisałem?

edytowany 1x, ostatnio: Mariuszkruk
Wibowit
  • Rejestracja:prawie 20 lat
  • Ostatnio:około 3 godziny
6

Problem jest złożony, bo jest wiele czynników wpływających na koszt przełączania kontekstu (ang. "context switch" i tego szukaj w google). Przełączenie kontekstu wiąże się np z koniecznością przeładowania danych, bo często poprzedni wątek wrzuca sobie do pamięci podręcznej L1, L2, L3, itd dane, które obecnemu wątkowi są niepotrzebne, a wyrzucił te które są potrzebne.

Przykładowe wyniki są np tutaj: http://www.cs.rochester.edu/u/cli/research/switch.pdf

W rzeczywistości nie tworzy się setek wątków, no chyba że korzystamy z blokującego I/O i działamy na setkach plików jednocześnie - takich sytuacji najlepiej unikać, bo tutaj jest ryzyko że sumaryczna wielkość stosów dla wszystkich wątków spowoduje zapchanie pamięci. Zamiast tworzenia setek wątków tworzy się pulę tylu wątków ile jest logicznych rdzeni procesora, a potem wrzuca się do tej puli wątków zadania.

W twoim przykładzie nawet gdyby tworzenie i przełączanie wątku miały zerowy narzut to i tak wersja wielowątkowa byłaby wolniejsza. Dzieje się tak dlatego, że współdzielony mutowalny stan musi być jakoś synchronizowany między wątkami. Tutaj można by użyć atomic integerów i w ten sposób uniknąć monitorów, ale i tak będzie spowolnienie w stosunku do jednego wątku. Inkrementacja atomic integera polega na operacji compare-and-swap. Jeśli wiele wątków jednocześnie zmodyfikuje atomic integera to tylko jednemu operacja się powiedzie za pierwszym razem, a reszta będzie musiała ponawiać operację, aż do skutku. Bez użycia atomic integerów czy innej metody synchronizacji współdzielonego mutowalnego stanu dostaniesz złe wyniki będące następstwem https://en.wikipedia.org/wiki/Race_condition

Aby uzyskać jak największe przyspieszenie z wielowątkowości trzeba przerobić algorytm tak, by wątki jak największą część czasu pracowały na oddzielnych danych - bez synchronizacji między sobą. Sprawdź np: https://en.wikipedia.org/wiki/Amdahl%27s_law


"Programs must be written for people to read, and only incidentally for machines to execute." - Abelson & Sussman, SICP, preface to the first edition
"Ci, co najbardziej pragną planować życie społeczne, gdyby im na to pozwolić, staliby się w najwyższym stopniu niebezpieczni i nietolerancyjni wobec planów życiowych innych ludzi. Często, tchnącego dobrocią i oddanego jakiejś sprawie idealistę, dzieli od fanatyka tylko mały krok."
Demokracja jest fajna, dopóki wygrywa twoja ulubiona partia.
edytowany 3x, ostatnio: Wibowit
twonek
  • Rejestracja:ponad 10 lat
  • Ostatnio:prawie 2 lata
  • Postów:2500
1

Zależy od procesora, systemu, architektury. W tym teoretycznym przykładzie koszt będzie wielokrotnie większy niż właściwa operacja, bo sama operacja jest trywialna, a zmiana kontekstu to co najmniej zapisanie/wczytanie rejestrów, bieżącej instrukcji itd. Wpisz w google'a thread context switch overhead i popatrz na 10 pierwszych linków.

Shalom
  • Rejestracja:około 21 lat
  • Ostatnio:prawie 3 lata
  • Lokalizacja:Space: the final frontier
  • Postów:26433
2

Wątki są generalnie dość "lekkie" i współdzielą dość sporo między sobą, w tym sekcje kodu i pamięć. W efekcie narzut będzie dość niewielki, bo możliwe że nawet cache będzie tu działać poprawnie i mimo context switcha wątku dostęp do danych i do instrukcji poleci z cache.
Nadal będzie to dużo wolniejsze niż ten jeden wątek oczywiście.
W rzeczywistości wątki często pracują na osobnych blokach pamięci i wtedy każdy context switch wymaga wymiany data cache. Dodatkowo często jeśli masz wiele wątków to robią one inne operacje więc wymaga to też wymiany instruction cache co context switch. W takiej sytuacji zaczyna sie to robić dużo cięższe.


"Nie brookliński most, ale przemienić w jasny, nowy dzień najsmutniejszą noc - to jest dopiero coś!"
edytowany 1x, ostatnio: Shalom
koszalek-opalek
  • Rejestracja:około 9 lat
  • Ostatnio:około 2 lata
1

Co do lekkości wątków -- w Linuxie na przykład można (szczegółów nie pamiętam teraz) ustawić sobie co dzielą wątki/procesy, a co mają osobne. W związku z tym niektóry wątki mogą być bardzo lekkie (dzielą prawie wszystko) i ich przełączanie będzie szybkie. Ale oczywiście wolniejsze niż nieprzełączanie. Na drugim krańcu mamy procesy czyli wątki ciężkie, które wszystko co się da mają osobne...

elwis
O ile dobrze pamiętam można to zrobić używając wywołania clone() zamiast fork() czy pthread. Btw zarówno pthread jak i fork() używają clone() pod spodem. W man clone – tam jest napisane jakie mamy opcje.
0

Kompletne bzdury opowiadasz - jak głupie dziecko!

Zwłaszcza to:

Aby zostać dobrze zrozumianym postaram się przybliżyć problem podając przykład(procesor 1 rdzeniowy), jest aplikacja a i aplikacja b. Aplikacja a posiada 10000 wątków(wątki powołane są na początku, tzn. nie kaskadowo podczas działania programu), każdy wątek wykonuje operacje dodania jedynki do statycznej zmiennej i rozpoczyna działanie kolejnego wątku.

Tworzenie wątków być może jest nieco bardziej czasochłonne od ich uruchamiania,
niemniej każda z tych operacji to grube setki instrukcji/operacji procesora;
a dodawanie to zaledwie jedna instrukcja - 1 takt!

Ale i to mało!
Przy używaniu - współdzieleniu jednej zmiennej przez wiele wątków,
musiałbyś bookować dostęp do tej zmiennej - to tzw. critical section w tej terminologii,
inaczej otrzymałbyś prędzej coś w stylu rosyjskiej ruletki, zamiast dodawania.

KR
Wielowątkowo zwiększać wartość zmiennej można bez sekcji krytycznej z użyciem CAS. Będzie to znacznie szybsze niż sekcja krytyczna, ale nadal dużo wolniejsze niż zwykle dodawanie.
MA
  • Rejestracja:prawie 8 lat
  • Ostatnio:prawie 5 lat
  • Postów:25
0
bjjjjjjjjjjjj napisał(a):

Kompletne bzdury opowiadasz - jak głupie dziecko!

Zwłaszcza to:

Aby zostać dobrze zrozumianym postaram się przybliżyć problem podając przykład(procesor 1 rdzeniowy), jest aplikacja a i aplikacja b. Aplikacja a posiada 10000 wątków(wątki powołane są na początku, tzn. nie kaskadowo podczas działania programu), każdy wątek wykonuje operacje dodania jedynki do statycznej zmiennej i rozpoczyna działanie kolejnego wątku.

Tworzenie wątków być może jest nieco bardziej czasochłonne od ich uruchamiania,
niemniej każda z tych operacji to grube setki instrukcji/operacji procesora;
a dodawanie to zaledwie jedna instrukcja - 1 takt!

Ale i to mało!
Przy używaniu - współdzieleniu jednej zmiennej przez wiele wątków,
musiałbyś bookować dostęp do tej zmiennej - to tzw. critical section w tej terminologii,
inaczej otrzymałbyś prędzej coś w stylu rosyjskiej ruletki, zamiast dodawania.

Chyba czegoś nie zrozumiałeś, ja tutaj nie wygłaszam żadnej tezy tylko pytam jak to wygląda. Nawet jeśli przykład nie jest najlepszy to obrazuje to co miałem na myśli.

edytowany 1x, ostatnio: Mariuszkruk
wil
  • Rejestracja:ponad 19 lat
  • Ostatnio:ponad 6 lat
0

Chyba czegoś nie zrozumiałeś, ja tutaj nie wygłaszam żadnej tezy tylko pytam jak to wygląda. Nawet jeśli przykład nie jest najlepszy to obrazuje to co miałem na myśli.

Obawiam się że ty nie rozumiesz podstaw logiki, że takie idiotycznie problemy rozważasz... i do tego publicznie. :)

KR
Moderator
  • Rejestracja:prawie 21 lat
  • Ostatnio:4 minuty
  • Postów:2964
2

Problem postawiony przez autora może nie został czytelnie sformułowany, ale jak najbardziej jest zasadne stawianie takich pytań.
Odpowiedź jest niestety dość skomplikowana.

Samo przełączenie kontekstu kosztuje zwykle tyle co syscall + troszkę pracy kernela nad tym aby zapisać stan wątku, zmodyfikować jego status (np. informację dlaczego wątek musi być wywłaszczony), znaleźć inny wątek do wykonania, załadować i przywrócić kontrolę do nowego wątku. To wyjdzie znacznie więcej niż proste dodawanie, ale myślę, że w kilku tysiącach cykli powinno się zamknąć.

To jednak nie jest najgorsze.
Większym problemem jest cache - nowy wątek zapewne wywali z cache procesora dane poprzedniego wątku. Sam za to będzie pracował na początku na zimnym cache, tj. spodziewaj się dużo nietrafień w cache, zwłaszcza jeśli drugi wątek operuje w obrębie innego kodu. Jeden cache miss to znowu setki jak nie tysiące cykli oczekiwania, aczkolwiek to się nie dodaje tak wprost, CPU potrafi przewidywać dostępy i ładować do cache zawczasu. Tak czy inaczej - tuż po przełączeniu wątek będzie pracował wolniej.

Teraz, jeżeli wątki nawzajem nie posiadają żadnej zależności, to OS będzie przełączał się między nimi na tyle często, aby sprawiać wrażenie równoczesności, ale na tyle rzadko aby nie powodowało to dużego narzutu. Zwykle kilkadziesiąt razy do tysiąca razy na sekundę. Wtedy sumaryczny koszt przełączania będzie niewielki, bo te kilkadziesiąt tysięcy taktów procesora zmarnowanych w wyniku przełączenia będzie niczym wobec milionów taktów dostępnych pomiędzy przełączaniem (zakładam, że procesor pracuje z częstotliwością rzędu GHz).

Dużo większy problem się pojawia, jeśli wątek zależy od innego wątku i musi się co chwilę zatrzymywać. Np. próbuje wejść w sekcję krytyczną albo blokująco czytać dane z socketa, który nie ma jeszcze danych. Wtedy wątek ręcznie wymusza przełączenie - tzn. wywołuje syscall odpowiedzialny za np. czytanie z socketa, a OS widzi że nie ma danych i usypia wątek do czasu jak dane się w buforze pojawią. Jeśli np. co tysiąc taktów będzie przełączanie kontekstu, bo wątek chodzi w ciasnej pętli, która ma w środku takiego "blockera", to wtedy się okaże, że większość czasu CPU spędza wykonując kod kernela przełączający wątki lub czekając na pamięć. W takiej sytuacji odpalenie większej liczby wątków nie zwiększy wydajności programu, a ją zmniejszy.

No i jest jeszcze jedna rzecz, niezwiązana z przełączaniem kontekstu:

każdy wątek wykonuje operacje dodania jedynki do statycznej zmiennej

Nie rób tak nigdy. Z dwóch powodów:

  • nie da to rezultatów, których oczekujesz - wyniki będą niepoprawne, bo dodawanie to "odczyt + dodawanie w rejestrach + zapis". Dwa wątki mogą odczytać tę samą wartość równocześnie i dodać do niej jeden - w ten sposób licznik będzie wskazywał mniej niż powinien.
  • zabije to wydajność na procesorach wielordzeniowych - dodawanie jest szybkie, ale zapis do tej samej linii cache przez dwa rdzenie to wydajnościowa masakra. Ponieważ cache w x86/amd64 jest koherentny, każdy zapis unieważnia linie cache w cache L1/L2 innych rdzeni. Zanim zapis będzie możliwy, linia cache musi być "czysta", więc musi być najpierw zassana z L3 aby można ją było zmodyfikować. Nie jest to tak duży koszt jak przełączenie kontekstu, ale porównaj sobie:
    • operacja dodawania intów: zwykle mniej niż takt (tzn. można nawet zrobić 4 w jednym; na różnych potokach, abstrahując tu na razie od SSE/AVX).
    • dostęp do cache L3: 65 cykli (40 cykli, jeśli linia nie jest współdzielona).

Do zliczania operacji z wielu wątków należy stosować odpowiedni algorytm użyty np. w LongAccumulator.

BTW: tu się czai kolejna pułapka, bo modyfikowanie dwóch różnych zmiennych przez różne wątki też może powodować ww efekt spadku wydajności - wystarczy, że obie zmienne leżą tuż bok siebie w pamięci, na tej samej linii cache - pogooglaj "false sharing".

edytowany 1x, ostatnio: Krolik
Zobacz pozostałe 7 komentarzy
Wibowit
Jak na swoim Ubuntu odpalę powerstat -D to przy bezczynności mam kilkutysięczne wartości w kolumnie Ctxt/s. Po włączeniu filmu na YouTube wartości skaczą do poziomu kilkudziesięciu tysięcy.
AF
@Krolik Myślałem, że masz na myśli kwant czasu, a nie przełączenia w przypadku bezrobocia, wtedy rzeczywiście Twoje liczby mają sens. Windows serwerowy ma kwant czasu na poziomie 200ms, linuks zazwyczaj miał 10ms, więc stąd moje pytanie, ale to wynik nieporozumienia.
KR
Masz rację, Linuks domyślnie daje 100 ms time-slice dla wątku, jeśli nie trzeba przełączyć kontekstu z innego powodu. Niemniej każdy syscall jest liczony jako przyłączenie kontekstu, więc liczby przełączen mogą być wysokie.
KR
@Mariuszkruk: pisze się kod nieblokująco, z wykorzystaniem epoll i wtedy nawet jednym wątkiem ogarniesz 500000 równoległych połączeń, po odpowiednim dostrojeniu kernela.
KR
No i jeszcze jest taka kwestia, że proces o wyższym priorytecie wywlaszcza procesy o niższym i właśnie ta decyzja jest podejmowana co 10 ms (w kernelach low latency chyba mniej).
Wibowit
  • Rejestracja:prawie 20 lat
  • Ostatnio:około 3 godziny
4

Z ciekawości sprawdziłem ile razy Windows 8.1 przełącza kontekst na sekundę. Żeby to sprawdzić trzeba wejść w Narzędzia Administracyjne, a potem Monitor Wydajności. Później trzeba przejść do wykresu (pokazane na zrzucie ekranu poniżej). Domyślnie na wykresie jest rysowany czas procesora, więc wskaźnik przełączeń kontekstu na sekundę trzeba sobie dorzucić. Efekt finalny jest taki (wyrzuciłem tutaj wskaźnik zajętości CPU):
ctxt_per_s.png
Komputer w zasadzie nie był obciążony, miałem tylko otwarte parę stron w przeglądarkach (w tym przez jakiś czas wideo na YouTube i jeden PDF). Ponadto antywirus i trochę badziewia w zasobniku, ale sumarycznie obciążenie CPU to pojedyczne procenty.

Procesor to Core i5-4670, a więc 4-rdzeniowy (bez HT).


"Programs must be written for people to read, and only incidentally for machines to execute." - Abelson & Sussman, SICP, preface to the first edition
"Ci, co najbardziej pragną planować życie społeczne, gdyby im na to pozwolić, staliby się w najwyższym stopniu niebezpieczni i nietolerancyjni wobec planów życiowych innych ludzi. Często, tchnącego dobrocią i oddanego jakiejś sprawie idealistę, dzieli od fanatyka tylko mały krok."
Demokracja jest fajna, dopóki wygrywa twoja ulubiona partia.
edytowany 1x, ostatnio: Wibowit
Kliknij, aby dodać treść...

Pomoc 1.18.8

Typografia

Edytor obsługuje składnie Markdown, w której pojedynczy akcent *kursywa* oraz _kursywa_ to pochylenie. Z kolei podwójny akcent **pogrubienie** oraz __pogrubienie__ to pogrubienie. Dodanie znaczników ~~strike~~ to przekreślenie.

Możesz dodać formatowanie komendami , , oraz .

Ponieważ dekoracja podkreślenia jest przeznaczona na linki, markdown nie zawiera specjalnej składni dla podkreślenia. Dlatego by dodać podkreślenie, użyj <u>underline</u>.

Komendy formatujące reagują na skróty klawiszowe: Ctrl+B, Ctrl+I, Ctrl+U oraz Ctrl+S.

Linki

By dodać link w edytorze użyj komendy lub użyj składni [title](link). URL umieszczony w linku lub nawet URL umieszczony bezpośrednio w tekście będzie aktywny i klikalny.

Jeżeli chcesz, możesz samodzielnie dodać link: <a href="link">title</a>.

Wewnętrzne odnośniki

Możesz umieścić odnośnik do wewnętrznej podstrony, używając następującej składni: [[Delphi/Kompendium]] lub [[Delphi/Kompendium|kliknij, aby przejść do kompendium]]. Odnośniki mogą prowadzić do Forum 4programmers.net lub np. do Kompendium.

Wspomnienia użytkowników

By wspomnieć użytkownika forum, wpisz w formularzu znak @. Zobaczysz okienko samouzupełniające nazwy użytkowników. Samouzupełnienie dobierze odpowiedni format wspomnienia, zależnie od tego czy w nazwie użytkownika znajduje się spacja.

Znaczniki HTML

Dozwolone jest używanie niektórych znaczników HTML: <a>, <b>, <i>, <kbd>, <del>, <strong>, <dfn>, <pre>, <blockquote>, <hr/>, <sub>, <sup> oraz <img/>.

Skróty klawiszowe

Dodaj kombinację klawiszy komendą notacji klawiszy lub skrótem klawiszowym Alt+K.

Reprezentuj kombinacje klawiszowe używając taga <kbd>. Oddziel od siebie klawisze znakiem plus, np <kbd>Alt+Tab</kbd>.

Indeks górny oraz dolny

Przykład: wpisując H<sub>2</sub>O i m<sup>2</sup> otrzymasz: H2O i m2.

Składnia Tex

By precyzyjnie wyrazić działanie matematyczne, użyj składni Tex.

<tex>arcctg(x) = argtan(\frac{1}{x}) = arcsin(\frac{1}{\sqrt{1+x^2}})</tex>

Kod źródłowy

Krótkie fragmenty kodu

Wszelkie jednolinijkowe instrukcje języka programowania powinny być zawarte pomiędzy obróconymi apostrofami: `kod instrukcji` lub ``console.log(`string`);``.

Kod wielolinijkowy

Dodaj fragment kodu komendą . Fragmenty kodu zajmujące całą lub więcej linijek powinny być umieszczone w wielolinijkowym fragmencie kodu. Znaczniki ``` lub ~~~ umożliwiają kolorowanie różnych języków programowania. Możemy nadać nazwę języka programowania używając auto-uzupełnienia, kod został pokolorowany używając konkretnych ustawień kolorowania składni:

```javascript
document.write('Hello World');
```

Możesz zaznaczyć również już wklejony kod w edytorze, i użyć komendy  by zamienić go w kod. Użyj kombinacji Ctrl+`, by dodać fragment kodu bez oznaczników języka.

Tabelki

Dodaj przykładową tabelkę używając komendy . Przykładowa tabelka składa się z dwóch kolumn, nagłówka i jednego wiersza.

Wygeneruj tabelkę na podstawie szablonu. Oddziel komórki separatorem ; lub |, a następnie zaznacz szablonu.

nazwisko;dziedzina;odkrycie
Pitagoras;mathematics;Pythagorean Theorem
Albert Einstein;physics;General Relativity
Marie Curie, Pierre Curie;chemistry;Radium, Polonium

Użyj komendy by zamienić zaznaczony szablon na tabelkę Markdown.

Lista uporządkowana i nieuporządkowana

Możliwe jest tworzenie listy numerowanych oraz wypunktowanych. Wystarczy, że pierwszym znakiem linii będzie * lub - dla listy nieuporządkowanej oraz 1. dla listy uporządkowanej.

Użyj komendy by dodać listę uporządkowaną.

1. Lista numerowana
2. Lista numerowana

Użyj komendy by dodać listę nieuporządkowaną.

* Lista wypunktowana
* Lista wypunktowana
** Lista wypunktowana (drugi poziom)

Składnia Markdown

Edytor obsługuje składnię Markdown, która składa się ze znaków specjalnych. Dostępne komendy, jak formatowanie , dodanie tabelki lub fragmentu kodu są w pewnym sensie świadome otaczającej jej składni, i postarają się unikać uszkodzenia jej.

Dla przykładu, używając tylko dostępnych komend, nie możemy dodać formatowania pogrubienia do kodu wielolinijkowego, albo dodać listy do tabelki - mogłoby to doprowadzić do uszkodzenia składni.

W pewnych odosobnionych przypadkach brak nowej linii przed elementami markdown również mógłby uszkodzić składnie, dlatego edytor dodaje brakujące nowe linie. Dla przykładu, dodanie formatowania pochylenia zaraz po tabelce, mogłoby zostać błędne zinterpretowane, więc edytor doda oddzielającą nową linię pomiędzy tabelką, a pochyleniem.

Skróty klawiszowe

Skróty formatujące, kiedy w edytorze znajduje się pojedynczy kursor, wstawiają sformatowany tekst przykładowy. Jeśli w edytorze znajduje się zaznaczenie (słowo, linijka, paragraf), wtedy zaznaczenie zostaje sformatowane.

  • Ctrl+B - dodaj pogrubienie lub pogrub zaznaczenie
  • Ctrl+I - dodaj pochylenie lub pochyl zaznaczenie
  • Ctrl+U - dodaj podkreślenie lub podkreśl zaznaczenie
  • Ctrl+S - dodaj przekreślenie lub przekreśl zaznaczenie

Notacja Klawiszy

  • Alt+K - dodaj notację klawiszy

Fragment kodu bez oznacznika

  • Alt+C - dodaj pusty fragment kodu

Skróty operujące na kodzie i linijkach:

  • Alt+L - zaznaczenie całej linii
  • Alt+, Alt+ - przeniesienie linijki w której znajduje się kursor w górę/dół.
  • Tab/⌘+] - dodaj wcięcie (wcięcie w prawo)
  • Shit+Tab/⌘+[ - usunięcie wcięcia (wycięcie w lewo)

Dodawanie postów:

  • Ctrl+Enter - dodaj post
  • ⌘+Enter - dodaj post (MacOS)