Chciałem wiedzieć to samo, więc zmierzyłem to. Na moim pudełku (ośmiordzeniowy procesor AMD FX (tm) -8150 przy 3,612361 GHz) blokowanie i odblokowywanie odblokowanego muteksu, który znajduje się we własnej linii pamięci podręcznej i jest już buforowany, zajmuje 47 zegarów (13 ns).
Ze względu na synchronizację między dwoma rdzeniami (użyłem CPU # 0 i # 1), mogłem wywołać parę blokowania / odblokowania tylko raz na 102 ns na dwóch wątkach, a więc raz na 51 ns, z czego można wywnioskować, że zajmuje to około 38 ns do odzyskania po odblokowaniu wątku, zanim następny wątek będzie mógł go ponownie zablokować.
Program, którego użyłem do zbadania tego, można znaleźć tutaj:
https://github.com/CarloWood/ai-statefultask-testsuite/blob/b69b112e2e91d35b56a39f41809d3e3de2f9e4b8/src/mutex_test.cxx
Zauważ, że ma kilka zakodowanych na stałe wartości specyficznych dla mojego pudełka (narzut xrange, yrange i rdtsc), więc prawdopodobnie będziesz musiał z nim poeksperymentować, zanim zadziała.
Wykres, który tworzy w tym stanie, to:
To pokazuje wynik testów porównawczych na następującym kodzie:
uint64_t do_Ndec(int thread, int loop_count)
{
uint64_t start;
uint64_t end;
int __d0;
asm volatile ("rdtsc\n\tshl $32, %%rdx\n\tor %%rdx, %0" : "=a" (start) : : "%rdx");
mutex.lock();
mutex.unlock();
asm volatile ("rdtsc\n\tshl $32, %%rdx\n\tor %%rdx, %0" : "=a" (end) : : "%rdx");
asm volatile ("\n1:\n\tdecl %%ecx\n\tjnz 1b" : "=c" (__d0) : "c" (loop_count - thread) : "cc");
return end - start;
}
Dwa wywołania rdtsc mierzą liczbę zegarów potrzebnych do zablokowania i odblokowania „muteksu” (z narzutem 39 zegarów dla wywołań rdtsc na moim pudełku). Trzeci asm to pętla opóźniająca. Rozmiar pętli opóźnienia jest o 1 liczbę mniejszy dla wątku 1 niż dla wątku 0, więc wątek 1 jest nieco szybszy.
Powyższa funkcja jest wywoływana w ciasnej pętli o rozmiarze 100 000. Pomimo tego, że funkcja jest nieco szybsza dla wątku 1, obie pętle synchronizują się z powodu wywołania muteksu. Na wykresie widać to po tym, że liczba zegarów zmierzona dla pary blokada / odblokowanie jest nieco większa dla wątku 1, aby uwzględnić krótsze opóźnienie w pętli poniżej.
Na powyższym wykresie dolny prawy punkt jest pomiarem z opóźnieniem loop_count równym 150, a następnie po punktach na dole, w lewo, loop_count jest zmniejszany o jeden na każdy pomiar. Gdy osiągnie wartość 77, funkcja jest wywoływana co 102 ns w obu wątkach. Jeśli następnie loop_count zostanie jeszcze bardziej zredukowany, nie będzie już możliwa synchronizacja wątków, a mutex zacznie być faktycznie blokowany przez większość czasu, co skutkuje zwiększoną liczbą zegarów potrzebnych do wykonania blokady / odblokowania. Z tego powodu wzrasta również średni czas wywołania funkcji; więc punkty wykresu idą teraz w górę i ponownie w prawo.
Z tego możemy wywnioskować, że blokowanie i odblokowywanie muteksu co 50 ns nie stanowi problemu na moim pudełku.
Podsumowując, mój wniosek jest taki, że odpowiedź na pytanie OP jest taka, że dodanie większej liczby muteksów jest lepsze, o ile prowadzi to do mniejszej rywalizacji.
Spróbuj zablokować muteksy tak krótkie, jak to możliwe. Jedynym powodem umieszczenia ich -powiedz- poza pętlą byłoby to, że pętla ta zapętla się szybciej niż raz na 100 ns (a raczej liczba wątków, które chcą uruchomić tę pętlę w tym samym czasie razy 50 ns) lub gdy 13 ns razy rozmiar pętli jest większym opóźnieniem niż opóźnienie otrzymywane przez rywalizację.
EDYCJA: Mam teraz dużo więcej wiedzy na ten temat i zaczynam wątpić w wniosek, który tutaj przedstawiłem. Po pierwsze, CPU 0 i 1 okazują się być hiperwątkowe; chociaż AMD twierdzi, że ma 8 prawdziwych rdzeni, z pewnością jest coś bardzo podejrzanego, ponieważ opóźnienia między dwoma innymi rdzeniami są znacznie większe (tj. 0 i 1 tworzą parę, podobnie jak 2 i 3, 4 i 5 oraz 6 i 7 ). Po drugie, std :: mutex jest zaimplementowany w taki sposób, że blokuje się na chwilę przed wykonaniem wywołań systemowych, gdy nie udaje mu się natychmiast uzyskać blokady muteksu (co bez wątpienia będzie bardzo wolne). Więc to, co tutaj zmierzyłem, to absolutnie najbardziej idealna lokalizacja, aw praktyce blokowanie i odblokowywanie może zająć drastycznie więcej czasu na zablokowanie / odblokowanie.
Podsumowując, mutex jest implementowany z atomiką. Aby zsynchronizować elementy atomowe między rdzeniami, należy zablokować wewnętrzną magistralę, która zamraża odpowiednią linię pamięci podręcznej na kilkaset cykli zegara. W przypadku, gdy nie można uzyskać blokady, należy wykonać wywołanie systemowe, aby uśpić wątek; to jest oczywiście bardzo wolne (wywołania systemowe są rzędu 10 mircosecond). Zwykle nie stanowi to problemu, ponieważ ten wątek i tak musi spać - ale może to być problem z dużą rywalizacją, w której wątek nie może uzyskać blokady na czas, w którym normalnie się obraca, tak samo jak wywołanie systemowe, ale MOŻE weź zamek wkrótce potem. Na przykład, jeśli kilka wątków blokuje i odblokowuje muteks w ciasnej pętli i każdy utrzymuje blokadę przez 1 mikrosekundę, wtedy mogą zostać ogromnie spowolnieni przez fakt, że są ciągle usypiani i ponownie budzeni. Ponadto, gdy wątek śpi i inny wątek musi go obudzić, ten wątek musi wykonać wywołanie systemowe i jest opóźniony o ~ 10 mikrosekund; to opóźnienie ma więc miejsce podczas odblokowywania muteksu, gdy inny wątek czeka na ten mutex w jądrze (po obróceniu trwało to zbyt długo).