Understanding std:: atomic:: compare exchange weak() in C++11

bool compare_exchange_weak (T& expected, T val, ..);

compare_exchange_weak() jest jednym z prymitywów compare-exchange udostępnianych w C++11. On słabe w tym sensie, że zwraca false, nawet jeśli wartość obiektu jest równa expected. Jest to spowodowane fałszywa porażka na niektórych platformach, gdzie Sekwencja instrukcji (zamiast takiej jak na x86) jest używana do jej implementacji. Na takich platformach przełącznik kontekstowy, przeładowanie tego samego adresu (lub linii pamięci podręcznej) przez inny wątek, itp.może zawieść prymityw. To spurious ponieważ to nie wartość obiektu (nie równa expected) nie powiedzie się operacji. Zamiast tego chodzi o problemy z wyczuciem czasu.

Ale to, co mnie zastanawia, to to, co zostało powiedziane w standardzie C++11 (ISO/IEC 14882),

29.6.5 .. Konsekwencją fałszywej porażki jest to, że prawie wszystkie zastosowania słabych compare-and-exchange będzie w pętli.

Dlaczego musi być w pętli w prawie wszystkie zastosowania ? Czy to oznacza, że będziemy pętli, gdy zawiedzie z powodu fałszywe porażki? Jeśli tak jest, to po co nam używanie compare_exchange_weak() i pisanie pętli sami? Możemy po prostu użyć compare_exchange_strong(), które moim zdaniem powinny pozbyć się fałszywych niepowodzeń dla nas. Jakie są najczęstsze przypadki użycia compare_exchange_weak()?

Kolejne pytanie związane. W swojej książce "współbieżność w C++ w działaniu" Anthony mówi:

//Because compare_exchange_weak() can fail spuriously, it must typically
//be used in a loop:

bool expected=false;
extern atomic<bool> b; // set somewhere else
while(!b.compare_exchange_weak(expected,true) && !expected);

//In this case, you keep looping as long as expected is still false,
//indicating that the compare_exchange_weak() call failed spuriously.

Dlaczego !expected jest w stanie pętli? Czy jest tam, aby zapobiec, że wszystkie wątki mogą głodować i nie robić postępów przez jakiś czas?

Edit: (ostatnia pytanie)

Na platformach, na których nie ma pojedynczej instrukcji sprzętowej CAS, zarówno wersja słaba jak i mocna są implementowane przy użyciu LL / SC (jak ARM, PowerPC, itp.). Czy jest jakaś różnica między następującymi dwoma pętlami? Dlaczego, jeśli w ogóle? (Dla mnie powinny mieć podobną wydajność.)

// use LL/SC (or CAS on x86) and ignore/loop on spurious failures
while (!compare_exchange_weak(..))
{ .. }

// use LL/SC (or CAS on x86) and ignore/loop on spurious failures
while (!compare_exchange_strong(..)) 
{ .. }

Wpadłem na to ostatnie pytanie, o którym wszyscy wspominacie, że może jest różnica w wydajności w pętli. Jest również wspomniany przez Standard C++11 (ISO/IEC 14882):

Gdy compare-and-exchange jest w pętli, słaba wersja uzyska lepsza wydajność na niektórych platformach.

Ale jak przeanalizowano powyżej, dwie wersje w pętli powinny dawać tę samą / podobną wydajność. Czego mi brakuje?

Author: Eric Z, 2014-08-08

4 answers

Po co robić exchange w pętli?

Zazwyczaj chcesz, aby Twoja praca została wykonana, zanim ruszysz dalej, dlatego umieszczasz compare_exchange_weak W pętli, aby próbowała się wymienić, dopóki się nie powiedzie(tzn. zwraca true).

Zauważ, że również compare_exchange_strong jest często używane w pętli. Nie ulega awarii z powodu fałszywej awarii, ale nie ulega awarii z powodu jednoczesnych zapisów.

Dlaczego używać weak zamiast strong?

Całkiem proste: fałszywa porażka nie zdarza się często, więc nie jest to duża wydajność hit. W constraście tolerowanie takiej awarii pozwala na znacznie wydajniejszą implementację wersji weak (w porównaniu do strong) na niektórych platformach: strong musi zawsze sprawdzać pod kątem fałszywej awarii i maskować ją. To jest drogie.

Tak więc, weak jest używany, ponieważ jest o wiele szybszy niż strong na niektórych platformach

Kiedy należy stosować weak i kiedy strong?

Odniesienie wskazuje, kiedy używać weak i kiedy używać strong:

Gdy compare-and-exchange jest w pętli, słaba wersja uzyska lepsza wydajność na niektórych platformach. Gdy słabe porównanie i wymiana wymagałaby pętli, a mocna nie, mocna jest lepiej.

Więc odpowiedź wydaje się być dość prosta do zapamiętania: jeśli musisz wprowadzić pętlę tylko z powodu fałszywej awarii, nie rób tego; użyj strong. Jeśli i tak masz pętlę, użyj weak.

Dlaczego !expected w przykład

To zależy od sytuacji i jej pożądanej semantyki, ale zwykle nie jest potrzebne do poprawności. Pominięcie tego daje bardzo podobną semantykę. Tylko w przypadku, gdy inny wątek może zresetować wartość false, semantyka może się nieco różnić (ale nie mogę znaleźć sensownego przykładu, w którym byś tego chciał). Zobacz komentarz Tony ' ego D., Aby uzyskać szczegółowe wyjaśnienie.

Jest to po prostu szybka ścieżka, gdy inny wątek pisze true: wtedy przerywamy zamiast próbować pisać true ponownie.

O Twoim ostatnim pytaniu

Ale jak przeanalizowano powyżej, dwie wersje w pętli powinny dawać tę samą / podobną wydajność. Czego mi brakuje?

From Wikipedia :

Rzeczywiste implementacje LL / SC nie zawsze się udają, jeśli nie ma równoczesne aktualizacje danych lokalizacji pamięci. Wszelkie wyjątkowe zdarzenia pomiędzy obiema operacjami, takie jak kontekst switch, another load-link, a nawet (na wielu platformach) inny load lub store działanie, spowoduje, że sklep-warunkowe do gwałtownej awarii. Starsze implementacje nie powiodą się, jeśli pojawią się jakiekolwiek aktualizacje transmitowane przez szyna pamięci.

Tak więc LL / SC zawiedzie na przykład przy przełączniku kontekstowym. Teraz mocna wersja wprowadzi swoją "własną małą pętlę", aby wykryć fałszywą awarię i zamaskować ją, próbując ponownie. Zauważ, że ta własna pętla jest również bardziej skomplikowana niż Zwykle pętla CAS, ponieważ musi odróżniać fałszywą awarię (i maskować ją) i awarię z powodu współbieżnego dostępu (co skutkuje zwrotem o wartości false). Słaba wersja nie ma takiej pętli.

Ponieważ podajesz jawną pętlę w obu przykładach, po prostu nie jest konieczne posiadanie małej pętli dla wersji strong. W związku z tym w przykładzie z wersją strong sprawdzanie błędu jest wykonywane dwa razy; raz przez compare_exchange_strong (co jest bardziej skomplikowane, ponieważ musi odróżnić fałszywych awarii i współbieżne acces) i raz przez pętli. Ten kosztowny czek jest niepotrzebny i powód, dla którego weak będzie tutaj szybszy.

Zauważ również, że twój argument (LL / SC) jest tylko jedną możliwością zaimplementowania tego. Jest więcej platform, które mają nawet różne zestawy instrukcji. Ponadto (i co ważniejsze), zauważ, że {[24] } musi obsługiwać wszystkie operacje dla wszystkich możliwych typów danych , więc nawet jeśli zadeklarujesz dziesięć milionów bajtów struct, możesz użyć compare_exchange na tym. Nawet jeśli na procesorze, który ma CAS, nie możesz CAS dziesięć milionów bajtów, więc kompilator wygeneruje inne instrukcje (prawdopodobnie lock acquire, następnie non-atomic compare and swap, a następnie lock release). Pomyśl, ile rzeczy może się wydarzyć podczas wymiany 10 milionów bajtów. Tak więc, podczas gdy fałszywy błąd może być bardzo rzadki w przypadku wymiany 8 bajtów, może być bardziej powszechny w tym przypadku.

Więc w skrócie, C++ daje dwie semantyki, a "najlepszy wysiłek" jeden (weak) i " zrobię to na pewno, bez względu na to, jak wiele złych rzeczy może się wydarzyć inbetween "jeden (strong). To, w jaki sposób są one wdrażane na różnych typach danych i platformach, to zupełnie inny temat. Nie przywiązuj swojego modelu mentalnego do implementacji na konkretnej platformie; biblioteka standardowa jest zaprojektowana do pracy z większą liczbą architektur, niż możesz być świadomy. Jedynym ogólnym wnioskiem, jaki możemy wyciągnąć jest to, że zagwarantowanie sukcesu jest zwykle trudniejsze (a więc może wymagają dodatkowej pracy) niż tylko próby i pozostawienie miejsca na ewentualną porażkę.

 60
Author: gexicide,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-08-11 08:30:25

Dlaczego musi być w pętli w prawie wszystkich zastosowaniach ?

Ponieważ jeśli nie zapętlisz pętli i zawiedzie, Twój program nie zrobił nic użytecznego - nie zaktualizowałeś obiektu atomowego i nie wiesz, jaka jest jego aktualna wartość (poprawka: patrz komentarz poniżej od Camerona). Jeśli połączenie nie robi nic pożytecznego, jaki jest sens tego robić?

Czy to oznacza, że będziemy pętli, gdy zawiedzie z powodu fałszywych porażki?

Tak.

Jeśli tak jest, to po co nam używanie compare_exchange_weak() i pisanie pętli sami? Możemy po prostu użyć funkcji compare_exchange_strong (), która moim zdaniem powinna pozbyć się fałszywych błędów. Jakie są typowe przypadki użycia compare_exchange_weak ()?

Na niektórych architekturach compare_exchange_weak jest bardziej wydajny, a fałszywe błędy powinny być dość rzadkie, więc może być możliwe pisanie bardziej wydajnych algorytmów przy użyciu słabej formy i pętla.

Ogólnie rzecz biorąc, prawdopodobnie lepiej jest użyć wersji silnej, jeśli algorytm nie wymaga pętli, ponieważ nie musisz się martwić o fałszywe błędy. Jeśli i tak musi zapętlić nawet dla wersji silnej (a wiele algorytmów i tak musi zapętlić), użycie słabej formy może być bardziej efektywne na niektórych platformach.

Dlaczego {[2] } jest w stanie pętli?

Wartość mogła zostać ustawiona na true przez inny wątek, więc nie chcesz zapętlać, próbując to ustawić.

Edit:

Ale jak przeanalizowano powyżej, dwie wersje w pętli powinny dawać tę samą / podobną wydajność. Czego mi brakuje?

Z pewnością jest oczywiste, że na platformach, na których możliwa jest fałszywa awaria, implementacja compare_exchange_strong musi być bardziej skomplikowana, aby sprawdzić fałszywą awarię i spróbować ponownie.

Słaba forma po prostu powraca po fałszywej porażce, nie próbuje ponownie.

 15
Author: Jonathan Wakely,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-08-08 17:33:31

Staram się odpowiedzieć na to sam, po przejrzeniu różnych zasobów internetowych (np. ten i Ten ), standardu C++11, a także udzielonych tutaj odpowiedzi.

Powiązane pytania są połączone (np., "dlaczego !oczekiwany ?" łączy się z "Po co umieszczać compare_exchange_weak() w pętli ?") i odpowiedzi udzielane są odpowiednio.


Dlaczego compare_exchange_weak () musi być w pętli w prawie wszystkie zastosowania?

Typowy Wzór A

Musisz uzyskać atomową aktualizację na podstawie wartości zmiennej atomowej. Błąd oznacza, że zmienna nie jest aktualizowana o pożądaną wartość i chcemy ją ponowić. Zauważ, że nie zależy nam na tym, czy zawiedzie z powodu równoczesnego zapisu czy fałszywej awarii. Ale zależy nam na tym to my to sprawia, że ta zmiana.

expected = current.load();
do desired = function(expected);
while (!current.compare_exchange_weak(expected, desired));

Prawdziwym przykładem jest dla kilku wątki, aby dodać element do pojedynczo połączonej listy jednocześnie. Każdy wątek najpierw ładuje wskaźnik head, przydziela nowy węzeł i dołącza go do nowego węzła. Na koniec próbuje zamienić nowy węzeł z głową.

Innym przykładem jest implementacja mutex za pomocą std::atomic<bool>. Co najwyżej jeden wątek może wejść do sekcji krytycznej na raz, w zależności od tego, który wątek najpierw ustaw current na true i wyjdź z pętli.

Typowy Wzór B

To jest właściwie wzór wymieniony w książce Anthony ' ego. W przeciwieństwie do wzorca A, chcesz, aby zmienna atomowa została zaktualizowana raz, ale nie obchodzi cię, kto to zrobi. dopóki nie jest aktualizowany, spróbuj ponownie. Jest to zwykle używane w przypadku zmiennych logicznych. Na przykład, trzeba zaimplementować wyzwalacz dla maszyny stanowej, aby przejść dalej. Który wątek pociągnie za spust jest bez znaczenia.

expected = false;
// !expected: if expected is set to true by another thread, it's done!
// Otherwise, it fails spuriously and we should try again.
while (!current.compare_exchange_weak(expected, true) && !expected);

Zauważ, że generalnie nie możemy użyć tego wzorca do implementacji mutex. W przeciwnym razie wiele wątków może znajdować się wewnątrz sekcja krytyczna w tym samym czasie.

To powiedziawszy, rzadko powinno się używać compare_exchange_weak() poza pętlą. Wręcz przeciwnie, istnieją przypadki, że wersja strong jest w użyciu. Np.,

bool criticalSection_tryEnter(lock)
{
  bool flag = false;
  return lock.compare_exchange_strong(flag, true);
}

compare_exchange_weak nie jest tu właściwe, ponieważ gdy wraca z powodu fałszywej awarii, prawdopodobnie nikt nie zajmuje jeszcze sekcji krytycznej.

Głodujący Wątek?

Warto wspomnieć, że co się stanie, jeśli fałszywe niepowodzenia nadal będą się dziać, tym samym głodząc wątek? Teoretycznie może się to zdarzyć na platformach, gdy compare_exchange_XXX() jest zaimplementowana jako sekwencja instrukcji (np. LL/SC). Częsty dostęp do tej samej linii pamięci podręcznej między LL I SC spowoduje ciągłe fałszywe awarie. Bardziej realistyczny przykład wynika z głupiego harmonogramu, w którym wszystkie równoległe wątki są przeplatane w następujący sposób.

Time
 |  thread 1 (LL)
 |  thread 2 (LL)
 |  thread 1 (compare, SC), fails spuriously due to thread 2's LL
 |  thread 1 (LL)
 |  thread 2 (compare, SC), fails spuriously due to thread 1's LL
 |  thread 2 (LL)
 v  ..

czy to możliwe?

Nie będzie to miało miejsca wiecznie, na szczęście, dzięki temu, czego wymaga C++11:]}

Implementacje powinny zapewnić, że słabe porównanie i wymiana operacje nie zwracają konsekwentnie false, chyba że albo atomic obiekt ma wartość inną niż oczekiwana lub są współbieżne modyfikacje obiektu atomowego.

Dlaczego zawracamy sobie głowę używaniem compare_exchange_weak () i sami piszemy pętlę? Możemy po prostu użyć compare_exchange_strong ().

To zależy.

Przypadek 1: gdy oba muszą być użyte wewnątrz pętli. C++11 says:

Gdy compare-and-exchange jest w pętli, słaba wersja uzyska lepsza wydajność na niektórych platformach.

Na x86 (przynajmniej obecnie. Może kiedyś będzie się odwoływać do podobnego schematu jak LL / SC dla wydajności, gdy wprowadzi się więcej rdzeni), słaba i mocna Wersja są zasadniczo takie same, ponieważ oba sprowadzają się do pojedynczej instrukcji cmpxchg. Na niektórych innych platformach gdzie {[10] }nie jest zaimplementowane atomicznie (tutaj znaczenie nie istnieje jeden prymityw sprzętowy), słaba wersja wewnątrz pętli może wygrać bitwę, ponieważ silny będzie musiał poradzić sobie z fałszywymi awariami i ponownie spróbować odpowiednio.

ale,

Rzadko możemy preferować compare_exchange_strong() nad compare_exchange_weak() nawet w pętli. Na przykład, gdy jest wiele rzeczy do zrobienia między zmienną atomową jest ładowana i obliczona nowa wartość jest wymieniana (patrz function() powyżej). Jeśli sama zmienna atomowa nie zmienia się często, nie trzeba powtórzyć kosztowne obliczenia dla każdej fałszywej awarii. Zamiast tego możemy mieć nadzieję, że compare_exchange_strong() "wchłonie" takie błędy i będziemy powtarzać obliczenia tylko wtedy, gdy nie powiedzie się z powodu rzeczywistej zmiany wartości.

Przypadek 2: tylko wtedy, gdy compare_exchange_weak() trzeba użyć wewnątrz pętli. C++11 says:

Gdy słabe porównanie i wymiana wymagałyby pętli i silnej nie, silniejszy jest lepszy.

Jest to zazwyczaj w przypadku pętli tylko po to, aby wyeliminować fałszywe awarie ze słabej wersji. Ponawiasz próbę, dopóki exchange nie zakończy się sukcesem lub nie powiedzie się z powodu współbieżnego zapisu.

expected = false;
// !expected: if it fails spuriously, we should try again.
while (!current.compare_exchange_weak(expected, true) && !expected);

W najlepszym razie, odkrywa koła i działa tak samo jak compare_exchange_strong(). Gorzej? to podejście nie wykorzystuje w pełni maszyn, które zapewniają nie-fałszywe porównania i wymiany w sprzęcie .

Ostatni, jeśli zapętlisz inne rzeczy (np. patrz "typowy wzór A" powyżej), to istnieje duża szansa, że compare_exchange_strong() zostaną również wprowadzone w pętlę, która prowadzi nas z powrotem do poprzedniego przypadku.

 12
Author: Eric Z,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-08-10 13:35:48

W porządku, więc potrzebuję funkcji, która wykonuje atomowe przesunięcie w lewo. Mój procesor nie ma do tego natywnej operacji, a biblioteka standardowa nie ma do tego funkcji, więc wygląda na to, że piszę własną. Idzie:

void atomicLeftShift(std::atomic<int>* var, int shiftBy)
{
    do {
        int oldVal = std::atomic_load(var);
        int newVal = oldVal << shiftBy;
    } while(!std::compare_exchange_weak(oldVal, newVal));
}

Są dwa powody, dla których pętla może być wykonywana więcej niż raz.

  1. ktoś inny zmienił zmienną, kiedy robiłem lewy shift. Wyniki moich obliczeń nie powinny być stosowane do zmiennej atomowej, ponieważ skutecznie wymazać czyjś zapis.
  2. [6]] mój procesor odbił się i słaby CAS gwałtownie zawiódł.
Nie obchodzi mnie, który. Przesunięcie w lewo jest na tyle szybkie, że równie dobrze mogę zrobić to ponownie, nawet jeśli awaria była fałszywa.

To, co jest mniej szybko, to dodatkowy kod, który silny CAS musi owijać wokół słabego CAS, aby być silnym. Ten kod nie robi wiele, gdy słaby CAS się powiedzie... ale kiedy się nie powiedzie, silny CAS musi zrobić kilka praca detektywistyczna w celu ustalenia, czy był to przypadek 1, Czy 2. Ta praca detektywistyczna przybiera formę drugiej pętli, w zasadzie wewnątrz mojej własnej pętli. Dwie zagnieżdżone pętle. Wyobraź sobie, że twój nauczyciel algorytmów patrzy na ciebie teraz.

I jak już wcześniej wspomniałem, nie obchodzi mnie wynik tej pracy detektywistycznej! Tak czy siak, zmienię kadrę. Tak więc używanie silnego CAS nie daje mi dokładnie nic, a traci niewielką, ale mierzalną wydajność.

Innymi słowy, słaby CAS jest używany do implementacji operacji atomic update. Strong CAS jest używany, gdy zależy ci na wyniku CAS.

 10
Author: Sneftel,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-08-08 15:53:29