Kiedy należy stosować mm sfence mm lfence i mm mfence

Przeczytałem "Intel Optimization guide Guide For Intel Architecture".

Jednak nadal nie mam pojęcia, kiedy powinienem użyć

_mm_sfence()
_mm_lfence()
_mm_mfence()

Czy ktoś może wyjaśnić, kiedy powinny być używane podczas pisania kodu wielowątkowego?

Author: Peter Cordes, 2010-12-27

4 answers

Zastrzeżenie : nie jestem w tym ekspertem. Wciąż próbuję się tego nauczyć. Ale ponieważ nikt nie odpowiedział w ciągu ostatnich dwóch dni, wydaje się, że eksperci od instrukcji ogrodzenia pamięci nie są obfite. Oto moje zrozumienie ...

Intel jest systemem pamięcisłabo uporządkowanym . Oznacza to, że twój program może wykonać

array[idx+1] = something
idx++

Ale zmiana na idx może być globalnie widoczna (np. dla wątków / procesów działających na innych procesorach) przed zmianą na array . Umieszczenie sfence pomiędzy dwoma instrukcjami zapewni kolejność zapisów wysyłanych do FSB.

Tymczasem uruchamia się kolejny procesor

newestthing = array[idx]

Może buforować pamięć dla array i ma starą kopię, ale pobiera zaktualizowany idx z powodu braku pamięci podręcznej. Rozwiązaniem jest użycie lfence tuż wcześniej, aby upewnić się, że obciążenia są zsynchronizowane.

Ten artykuł lub ten artykuł może dać lepsze Informacje

 0
Author: Mark Borgerding,
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
2010-12-29 14:04:25

Jeśli używasz sklepów NT, możesz chcieć _mm_sfence, A może nawet _mm_mfence. Przypadki użycia _mm_lfence są znacznie bardziej niejasne.

Jeśli nie, po prostu użyj C++11 std:: atomic i pozwól kompilatorowi martwić się o szczegóły ASM sterowania porządkowaniem pamięci.


X86 ma silnie uporządkowany model pamięci, ale C++ ma bardzo słaby model pamięci (to samo dotyczy C). aby uzyskać/wydać semantykę, musisz tylko zapobiec zmianie kolejności w czasie kompilacji. # Patrz Jeff Preshing porządkowanie pamięci w czasie kompilacji artykuł.

_mm_lfence i _mm_sfence mają niezbędny efekt bariery kompilatora, ale również spowodują, że kompilator wyemituje bezużyteczną instrukcję lfence lub sfence ASM, która sprawia, że kod działa wolniej.

Są lepsze opcje do kontrolowania zmiany kolejności kompilacji, gdy nie robisz żadnych niejasnych rzeczy, które sprawią, że będziesz chciał sfence.

Na przykład GNU C / C++ asm("" ::: "memory") jest barierą kompilatora (wszystkie wartości muszą być w pamięć pasująca do maszyny abstrakcyjnej z powodu "memory" clobber), ale nie są emitowane instrukcje asm.

Jeśli używasz C++11 std:: atomic, możesz po prostu zrobić shared_var.store(tmp, std::memory_order_release). To gwarantuje globalnie widoczne Po wszelkich wcześniejszych przypisaniach C, nawet dla zmiennych nieatomowych.

_mm_mfence jest potencjalnie przydatne, jeśli nagrywasz własną wersję C11 / C++11 std::atomic, ponieważ rzeczywista Instrukcja mfence jest jednym ze sposobów na uzyskanie sekwencyjnego konsystencja, tzn. zatrzymanie późniejszych ładowań przed odczytem wartości, aż po uprzednim stanie się globalnie widoczne. Zobacz Jeff Preshing ' S reordering pamięci złapany w Akcie .

Ale zauważ, że mfence wydaje się być wolniejszy na obecnym sprzęcie niż użycie zablokowanej operacji atomic-RMW. na przykład xchg [mem], eax jest również pełną barierą, ale działa szybciej i robi sklep. Na Skylake, sposób mfence jest zaimplementowany zapobiega out-of-order wykonywania instrukcji nawet poza pamięcią po nim. Zobacz też na dole tej odpowiedzi .

W C++ bez wbudowanego asm, jednak opcje barier pamięci są bardziej ograniczone ( ile instrukcji barier pamięci ma procesor x86?). mfence nie jest straszne, i to jest to, czego gcc i clang używają obecnie do tworzenia sklepów o sekwencyjnej konsystencji.

Poważnie po prostu użyj C++11 std:: atomic lub C11 stdatomic, jeśli to możliwe, chociaż; jest łatwiejszy w użyciu i masz całkiem dobry code-gen do wielu rzeczy. Lub w Linuksie kernel, są już funkcje wrapper dla inline asm dla niezbędnych barier. Czasami jest to po prostu bariera kompilatora, czasami jest to również instrukcja asm, aby uzyskać silniejszą kolejność uruchamiania niż domyślna. (np. dla pełnej bariery).


Brak barier sprawi, że Twoje sklepy pojawią się w innych wątkach szybciej. Wszystko, co mogą zrobić, to opóźnić późniejsze operacje w bieżącym wątku, aż do wcześniejszego zdarzenia. Procesor już próbuje zatwierdzić oczekujące nie spekulacyjne sklepy do pamięci podręcznej L1d tak szybko, jak to możliwe.


[1] } jest zdecydowanie najbardziej prawdopodobną barierą do ręcznego użycia w C++[84]}

Główny przypadek użycia _mm_sfence() znajduje się po niektórych sklepach _mm_stream, przed ustawieniem flagi, którą sprawdzą inne wątki.

[[38]}Zobacz Enhanced REP MOVSB dla memcpy aby dowiedzieć się więcej o sklepach NT vs.zwykłych sklepach i przepustowości PAMIĘCI x86. Do zapisu bardzo dużych buforów (większych niż rozmiar bufora L3), które zdecydowanie nie będą ponownie odczytywane wkrótce dobrym pomysłem może być skorzystanie ze sklepów NT.

Sklepy NT są słabo uporządkowane, w przeciwieństwie do zwykłych sklepów, więc trzeba sfence jeśli zależy ci na opublikowaniu danych w innym wątku. jeśli nie (w końcu przeczytasz je z tego wątku), to nie. lub jeśli wykonasz wywołanie systemowe przed poinformowaniem innego wątku, że dane są gotowe, to również serializacja.

sfence (lub jakąś inną barierę) jest konieczne, aby dać ci zwolnienie / nabyć synchronizację podczas korzystania Sklepy NT. C++11std::atomic implementacje pozostawiają Tobie ogrodzenie sklepów NT , tak aby atomic release-stores mógł być wydajny.

#include <atomic>
#include <immintrin.h>

struct bigbuf {
    int buf[100000];
    std::atomic<unsigned> buf_ready;
};

void producer(bigbuf *p) {
  __m128i *buf = (__m128i*) (p->buf);

  for(...) {
     ...
     _mm_stream_si128(buf,   vec1);
     _mm_stream_si128(buf+1, vec2);
     _mm_stream_si128(buf+2, vec3);
     ...
  }

  _mm_sfence();    // All weakly-ordered memory shenanigans stay above this line
  // So we can safely use normal std::atomic release/acquire sync for buf
  p->buf_ready.store(1, std::memory_order_release);
}

Wtedy konsument może bezpiecznie zrobić if(p->buf_ready.load(std::memory_order_acquire)) { foo = p->buf[0]; ... } bez żadnych danych-Race niezdefiniowanego zachowania. Strona czytelnika nie potrzebuje ; słabo uporządkowany charakter sklepów NT ogranicza się wyłącznie do rdzenia zajmującego się pisaniem. Gdy staje się globalnie widoczny, jest w pełni spójny i uporządkowany zgodnie z normalnym Zasady.

Inne przypadki użycia obejmują nakazanie clflushopt kontrolowania kolejności danych przechowywanych w nieulotnej pamięci mapowanej w pamięci. (np. NVDIMM z pamięcią Optane lub DIMM Z bateryjną pamięcią DRAM istnieją już teraz.)


_mm_lfence prawie nigdy nie jest użyteczny jako rzeczywiste ogrodzenie obciążenia . Ładunki mogą być słabo uporządkowane tylko podczas ładowania z obszarów pamięci WC (Write-Combining), takich jak video ram. Parzyste movntdqa (_mm_stream_load_si128) jest nadal mocno uporządkowany na normalnym (WB = write-back) pamięci i nie robi nic, aby zmniejszyć zanieczyszczenie pamięci podręcznej. (prefetchnta może, ale trudno to dostroić i może pogorszyć sprawę.)

TL: DR: jeśli nie piszesz sterowników graficznych lub czegoś innego, co bezpośrednio mapuje pamięć RAM Wideo, nie potrzebujesz _mm_lfence, aby zamówić swoje ładunki.

lfence ma ciekawe działanie mikroarchitekturalne polegające na uniemożliwieniu wykonania późniejszych instrukcji do czasu przejścia na emeryturę. np. aby zatrzymać _rdtsc() odczyt licznika cykli podczas wcześniejszej pracy / align = "left" / (Dotyczy zawsze procesorów Intela, ale tylko AMD z ustawieniem MSR: czy lfence serializuje się na procesorach AMD?. W przeciwnym razie lfence uruchamia 4 na zegarze na rodzinie spychaczy, więc najwyraźniej nie serializuje.)

Ponieważ używasz intrinsics z C/C++, kompilator generuje kod dla Ciebie. Nie masz bezpośredniej kontroli nad asm, ale możesz użyć _mm_lfence do takich rzeczy jak łagodzenie widm, jeśli możesz zmusić kompilator do umieszczenia go w właściwe miejsce w wyjściu asm: zaraz po gałęzi warunkowej, przed dostępem do podwójnej tablicy. (jak foo[bar[i]]). Jeśli używasz łat na jądro dla Spectre, myślę, że jądro będzie bronić twojego procesu przed innymi procesami, więc musisz się o to martwić tylko w programie, który używa piaskownicy JIT i martwi się, że zostanie zaatakowany z jego własnego piaskownicy.

 5
Author: Peter Cordes,
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
2018-08-28 05:27:09

Oto moje zrozumienie, mam nadzieję, że wystarczająco dokładne i proste, aby mieć sens: {]}

(Itanium) Architektura IA64 pozwala na wykonywanie odczytów i zapisów pamięci w dowolnej kolejności, więc kolejność zmian pamięci z punktu widzenia innego procesora nie jest przewidywalna, chyba że użyjesz ogrodzeń, aby wymusić, że zapis jest kompletny w rozsądnej kolejności.

Od teraz mówię o x86, x86 jest mocno uporządkowany.

Na x86, Intel nie gwarantuje, że sklep zrobiony na innym procesorze zawsze będą natychmiast widoczne na tym procesorze. Jest możliwe, że ten procesor spekulatywnie wykonał Ładowanie (odczyt) wystarczająco wcześnie, aby przegapić magazyn (zapis) innego procesora. Gwarantuje to tylko, że kolejność zapisów staje się widoczna dla innych procesorów jest w kolejności programowej. Nie gwarantuje to, że inne procesory natychmiast zobaczą każdą aktualizację, bez względu na to, co zrobisz.

Zablokowane instrukcje odczytu/modyfikacji/zapisu są w pełni spójne sekwencyjnie. Z tego powodu, na ogół zajmujesz się już pominięciem operacji pamięci innego procesora, ponieważ zablokowane xchg lub cmpxchg zsynchronizują to wszystko, natychmiast uzyskasz odpowiednią linię pamięci podręcznej i zaktualizujesz ją atomicznie. Jeśli inny procesor ściga się z zablokowaną operacją, albo wygrasz wyścig, a drugi procesor przegapi pamięć podręczną i odzyska ją po zablokowanej operacji, albo wygra wyścig, a Ty przegapisz pamięć podręczną i otrzymasz zaktualizowaną wartość z oni.

lfence Wydanie instrukcji do czasu zakończenia wszystkich instrukcji przed lfence. mfence oczekuje, że wszystkie poprzednie odczyty pamięci zostaną w pełni wprowadzone do rejestru docelowego i oczekuje, że wszystkie poprzednie zapisy staną się globalnie widoczne, ale nie opóźnia wszystkich dalszych instrukcji, Jak to zrobiłoby lfence. sfence robi to samo dla only stores, flush write combiner i zapewnia, że wszystkie sklepy poprzedzające sfence są globalnie widoczne przed zezwoleniem na jakiekolwiek sklepy po sfence, aby rozpocząć wykonywanie.

Ogrodzenia wszelkiego rodzaju są rzadko potrzebne na x86, nie są konieczne, chyba że używasz pamięci łączącej zapis lub instrukcji nie-czasowych, co rzadko robisz, jeśli nie jesteś programistą trybu jądra (sterownika). Normalnie, x86 gwarantuje, że wszystkie sklepy są widoczne w kolejności programów, ale nie daje tej gwarancji dla pamięci WC (write combination) lub dla instrukcji "non-temporal", które nie mają wyraźnie uporządkowanych sklepów, takich jak movnti.

Tak więc, podsumowując, sklepy są zawsze widoczne w kolejności programów, chyba że użyłeś specjalnych słabo uporządkowanych sklepów lub masz dostęp do pamięci typu WC. Algorytmy wykorzystujące zablokowane instrukcje, takie jak xchg, xadd, Czy cmpxchg itd., będą działać bez ogrodzeń, ponieważ zablokowane instrukcje są kolejno spójne.

 4
Author: doug65536,
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
2018-06-09 14:37:09

Wewnętrzne wywołania wspominasz wszystkie po prostu wstaw an sfence, lfence lub mfence instrukcji, gdy są wywoływane. Więc pytanie staje się "jakie są cele tych instrukcji ogrodzenia"?

Krótka odpowiedź jest taka, że lfence jest całkowicie bezużyteczna* i sfence prawie całkowicie bezużyteczne dla celów porządkowania pamięci dla programów w trybie użytkownika w x86. Z drugiej strony, mfence służy jako pełna bariera pamięci, więc możesz jej używać w miejscach, w których potrzebujesz bariery, jeśli nie ma jeszcze jakiejś pobliskiej instrukcji lock-prefiksowej zapewniającej to, czego potrzebujesz.

Dłuższa, ale wciąż krótka odpowiedź brzmi...

Lfence

lfence jest udokumentowane, aby zamówić ładunki przed lfence w odniesieniu do ładunków po, ale ta gwarancja jest już zapewniona dla normalnych ładunków bez żadnego ogrodzenia w ogóle: to znaczy, Intel już gwarantuje, że "ładunki nie są zmieniane z innymi ładunkami". W praktyce pozostawia to cel lfence w kod trybu użytkownika jako bariera wykonania poza zleceniem, przydatny być może do dokładnego pomiaru czasu niektórych operacji.

Sfence

sfence jest udokumentowane zamawianie sklepów przed i po w taki sam sposób, jak lfence robi dla ładunków, ale podobnie jak ładunki zamówienie sklepu jest już gwarantowane w większości przypadków przez Intel. Podstawowym interesującym przypadkiem, w którym nie ma, są tzw. sklepy Pozaziemskie, takie jak movntdq, movnti, maskmovq i kilka innych instrukcje. Instrukcje te nie działają zgodnie z normalnymi regułami porządkowania pamięci, więc możesz umieścić sfence pomiędzy tymi sklepami a innymi sklepami, w których chcesz wyegzekwować względny porządek. mfence działa również w tym celu, ale sfence jest szybszy.

Mfence

W przeciwieństwie do dwóch pozostałych, mfence faktycznie coś robi: służy jako pełna bariera pamięci, zapewniając, że wszystkie poprzednie ładunki i magazyny zostaną zakończone1 przed kolejnymi ładunkami lub magazynami rozpocząć egzekucję. Ta odpowiedź jest zbyt krótka, aby w pełni wyjaśnić pojęcie bariery pamięci, ale przykładem może być algorytm Dekkera , w którym każdy wątek, który chce wejść do sekcji krytycznej, zapisuje się do lokalizacji, a następnie sprawdza, czy inny wątek zapisał coś do swojej lokalizacji. Na przykład w wątku 1:

mov   DWORD [thread_1_wants_to_enter], 1  # store our flag
mov   eax,  [thread_2_wants_to_enter]     # check the other thread's flag
test  eax, eax
jnz   retry
; critical section

Tutaj, na x86, potrzebujesz bariery pamięci pomiędzy store (pierwszym mov), a load (drugim mov), w przeciwnym razie każdy wątek może zobaczyć zero, gdy odczytują flagę drugiej strony, ponieważ model pamięci x86 pozwala na ponowne uporządkowanie obciążeń z wcześniejszymi zapasami. Można więc wstawić bariera mfence w następujący sposób, aby przywrócić sekwencyjną spójność i prawidłowe zachowanie algorytmu:

mov   DWORD [thread_1_wants_to_enter], 1  # store our flag
mfence
mov   eax,  [thread_2_wants_to_enter]     # check the other thread's flag
test  eax, eax
jnz   retry
; critical section

W praktyce nie widzisz mfence tyle, ile możesz się spodziewać, ponieważ instrukcje x86 lock-prefiks mają ten sam efekt pełnej bariery, a są to często/zawsze (?) tańsze niż mfence.


1 Np. ładunki zostaną zaspokojone, a sklepy staną się widoczne globalnie (chociaż byłoby to realizowane inaczej, o ile widoczny efekt zamówienia wrt jest "tak, jakby", który wystąpił).

 2
Author: BeeOnRope,
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
2018-06-09 20:25:16