Bariera pamięci

W informatyce bariera pamięci , znana również jako membar , ogrodzenie pamięci lub instrukcja ogrodzenia , jest rodzajem instrukcji bariery , która powoduje, że jednostka centralna (CPU) lub kompilator wymusza ograniczenie porządkowania operacji pamięci wydanych przed i po instrukcji bariery. Zwykle oznacza to, że operacje wydane przed barierą mają gwarancję wykonania przed operacjami wydanymi po barierze.

Bariery pamięciowe są konieczne, ponieważ większość nowoczesnych procesorów wykorzystuje optymalizacje wydajności, które mogą skutkować wykonywaniem zadań poza kolejnością . Ta zmiana kolejności operacji pamięciowych (ładowanie i przechowywanie) zwykle pozostaje niezauważona w ramach pojedynczego wątku wykonania , ale może powodować nieprzewidywalne zachowanie współbieżnych programów i sterowników urządzeń , chyba że jest dokładnie kontrolowana. Dokładny charakter ograniczenia porządkowania zależy od sprzętu i jest zdefiniowany przez model porządkowania pamięci w architekturze . Niektóre architektury zapewniają wiele barier dla wymuszania różnych ograniczeń porządkowania.

kodu maszynowego niskiego poziomu, który działa na pamięci współdzielonej przez wiele urządzeń. Taki kod obejmuje synchronizacji i wolne od blokad struktury danych w systemach wieloprocesorowych oraz sterowniki urządzeń, które komunikują się ze sprzętem komputerowym .

Przykład

Kiedy program działa na maszynie jednoprocesorowej, sprzęt wykonuje niezbędne księgowanie, aby zapewnić, że program jest wykonywany tak, jakby wszystkie operacje na pamięci były wykonywane w kolejności określonej przez programistę (kolejność programu), więc bariery pamięci nie są konieczne. Jednak gdy pamięć jest współdzielona z wieloma urządzeniami, takimi jak inne procesory w systemie wieloprocesorowym lub urządzenia peryferyjne mapowane w pamięci , dostęp poza kolejnością może wpływać na zachowanie programu. Na przykład, drugi CPU może zobaczyć zmiany pamięci dokonane przez pierwszy CPU w kolejności, która różni się od kolejności programu.

Program jest uruchamiany przez proces, który może być wielowątkowy (tj. wątek programowy, taki jak pthreads , w przeciwieństwie do wątku sprzętowego). Różne procesy nie współdzielą przestrzeni pamięci, więc ta dyskusja nie dotyczy dwóch programów, z których każdy działa w innym procesie (stąd inna przestrzeń pamięci). Ma zastosowanie do dwóch lub więcej wątków (programowych) uruchomionych w jednym procesie (tj. pojedynczej przestrzeni pamięci, w której wiele wątków oprogramowania współdzieli jedną przestrzeń pamięci). Wiele wątków oprogramowania w ramach jednego procesu może działać jednocześnie na procesorze wielordzeniowym .

Poniższy program wielowątkowy działający na procesorze wielordzeniowym jest przykładem tego, jak takie wykonanie poza kolejnością może wpłynąć na zachowanie programu:

0 Początkowo lokalizacje pamięci x i f mają wartość . Wątek programowy działający na procesorze nr 1 zapętla się, gdy wartość f wynosi zero, a następnie wypisuje wartość x . Wątek programowy działający na procesorze nr 2 przechowuje wartość 42 w x , a następnie zapisuje wartość 1 w f . Pseudo-kod dla dwóch fragmentów programu pokazano poniżej.

Kroki programu odpowiadają poszczególnym instrukcjom procesora.

Wątek nr 1 Rdzeń nr 1:

    0
 
   podczas gdy  (  fa  ==  );  // Wymagane tutaj ogrodzenie pamięci  print  x  ; 

Wątek nr 2 Rdzeń nr 2:

   
 
    x  =  42  ;  // Wymagane tutaj ogrodzenie pamięci  f  =  1  ; 

Można by oczekiwać, że instrukcja print zawsze wypisze liczbę „42”; jeśli jednak operacje magazynu wątku nr 2 są wykonywane poza kolejnością, f może zostać zaktualizowane przed x , a zatem instrukcja print może wypisać „0”. Podobnie operacje ładowania wątku nr 1 mogą być wykonywane poza kolejnością i możliwe jest odczytanie x przed f jest zaznaczone i ponownie instrukcja print może wydrukować nieoczekiwaną wartość. W przypadku większości programów żadna z tych sytuacji nie jest dopuszczalna. Bariera pamięci musi zostać wstawiona przed przypisaniem wątku nr 2 do f , aby zapewnić, że nowa wartość x będzie widoczna dla innych procesorów w momencie lub przed zmianą wartości f . Innym ważnym punktem jest to, że należy również wstawić barierę pamięci przed dostępem wątku nr 1 do x , aby upewnić się, że wartość x nie zostanie odczytana przed zobaczeniem zmiany wartości f .

Innym przykładem jest sytuacja, w której kierowca wykonuje następującą sekwencję:

       przygotuj  dane  dla  modułu  sprzętowego  //  wymagane  tutaj  ogrodzenie pamięci  wyzwala  moduł  sprzętowy  do  przetwarzania  danych 
 
        

Jeśli operacje przechowywania procesora są wykonywane poza kolejnością, moduł sprzętowy może zostać wyzwolony, zanim dane będą gotowe w pamięci.

Aby zapoznać się z innym ilustrującym przykładem (nietrywialnym, który pojawia się w rzeczywistej praktyce), zobacz podwójnie sprawdzone blokowanie .

Programowanie wielowątkowe i widoczność pamięci

Programy wielowątkowe zwykle używają prymitywów synchronizacji dostarczanych przez środowisko programistyczne wysokiego poziomu, takie jak Java i .NET Framework , lub interfejs programowania aplikacji (API), taki jak POSIX Threads lub Windows API . Elementy podstawowe synchronizacji, takie jak muteksy i semafory, służą do synchronizowania dostępu do zasobów z równoległych wątków wykonywania. Te prymitywy są zwykle implementowane z barierami pamięci wymaganymi do zapewnienia oczekiwanej semantyki widoczności pamięci . W takich środowiskach jawne stosowanie barier pamięci nie jest na ogół konieczne.

Zasadniczo każdy interfejs API lub środowisko programistyczne ma swój własny model pamięci wysokiego poziomu, który definiuje jego semantykę widoczności pamięci. Chociaż programiści zwykle nie muszą używać barier pamięci w środowiskach o tak wysokim poziomie, ważne jest zrozumienie ich semantyki widoczności pamięci w możliwym zakresie. Takie zrozumienie niekoniecznie jest łatwe do osiągnięcia, ponieważ semantyka widoczności pamięci nie zawsze jest spójnie określona lub udokumentowana.

Tak jak semantyka języka programowania jest definiowana na innym poziomie abstrakcji niż kody operacyjne języka maszynowego , tak model pamięci środowiska programistycznego jest definiowany na innym poziomie abstrakcji niż model pamięci sprzętowej. Ważne jest, aby zrozumieć to rozróżnienie i zdać sobie sprawę, że nie zawsze istnieje proste odwzorowanie między semantyką sprzętowej bariery pamięci niskiego poziomu a semantyką widoczności pamięci wysokiego poziomu w określonym środowisku programistycznym. W rezultacie implementacja wątków POSIX na określonej platformie może wymagać silniejszych barier niż wymaga tego specyfikacja. Programy korzystające z widoczności pamięci w postaci zaimplementowanej, a nie określonej, mogą nie być przenośne.

Wykonywanie poza kolejnością a optymalizacje zmiany kolejności kompilatora

Instrukcje bariery pamięci odnoszą się do efektów zmiany kolejności tylko na poziomie sprzętowym. Kompilatory mogą również zmieniać kolejność instrukcji w ramach optymalizacji programu . Chociaż wpływ na zachowanie programu równoległego może być podobny w obu przypadkach, generalnie konieczne jest podjęcie oddzielnych środków w celu zablokowania optymalizacji zmiany kolejności kompilatora dla danych, które mogą być współużytkowane przez wiele wątków wykonania.

W C i C++ słowo kluczowe volatile miało na celu umożliwienie programom C i C++ bezpośredniego dostępu do operacji we/wy mapowanych w pamięci. We/wy mapowane w pamięci ogólnie wymaga, aby odczyty i zapisy określone w kodzie źródłowym odbywały się dokładnie w określonej kolejności, bez żadnych pominięć. Pominięcia lub zmiany kolejności odczytów i zapisów przez kompilator zerwałyby komunikację między programem a urządzeniem, do którego dostęp uzyskuje się za pomocą we/wy mapowanych w pamięci. Kompilator AC lub C++ nie może pomijać odczytów i zapisów w ulotnych lokalizacjach pamięci, ani nie może zmieniać kolejności odczytu/zapisu względem innych takich działań dla tej samej ulotnej lokalizacji (zmiennej). Słowo kluczowe volatile nie gwarantuje bariery pamięci w celu wymuszenia spójności pamięci podręcznej. Dlatego samo użycie zmiennej volatile nie wystarczy do użycia zmiennej do komunikacji między wątkami we wszystkich systemach i procesorach.

Standardy C i C++ wcześniejsze niż C11 i C++11 nie dotyczą wielu wątków (lub wielu procesorów), dlatego użyteczność volatile zależy od kompilatora i sprzętu. Chociaż ulotność gwarantuje, że ulotne odczyty i ulotne zapisy będą miały miejsce w dokładnej kolejności określonej w kodzie źródłowym, kompilator może wygenerować kod (lub procesor może zmienić kolejność wykonywania) w taki sposób, że ulotna kolejność odczytu lub zapisu zostanie zmieniona w odniesieniu do nie -volatile odczytuje lub zapisuje, ograniczając w ten sposób jego użyteczność jako flagi lub muteksu między wątkami. Zapobieganie temu jest specyficzne dla kompilatora, ale niektóre kompilatory, takie jak gcc , nie zmieni kolejności operacji wokół wbudowanego kodu asemblera z tagami volatile i "memory" , jak w: asm volatile ("" ::: "memory"); (Zobacz więcej przykładów w Porządkowanie pamięci#Porządkowanie pamięci w czasie kompilacji ).

Zobacz też

Linki zewnętrzne