Programowanie współbieżne, Języki programowania, Aplikacje, Gry

Widok wątku na pamięć

Na wstępie zachęcam do przeczytania artykułu, o którym pisałem w poprzednim poście. Osobom nie zorientowanym w temacie programowania wielowątkowego znacząco ułatwi zrozumienie tego co będę chciał poniżej przedstawić.

Aby zrozumieć co oznacza „widok pamięci wątku” warto przeanalizować krótki program w języku C++:

int a;
// --- instrukcje ---
a += 2;
a += 3;

Zakładamy:

  • a jest zmienną, dla której została zaalokowana komórka w pamięci komputera
  • wartość tej komórki przed wykonaniem instrukcji (3) wynosi 0 (tzn że wartość a wynosi 0)
  • tylko jeden wątek wykonuje kod przedstawiony powyżej, żaden inny wątek nie modyfikuje a
  • rozpatrujemy program napisany w języku C++, skompilowany i uruchomiony na popularnych procesorach rodziny x86 (Athlon 64, Core 2, Pentium itp …)

Należy się zastanowić kiedy wynik instrukcji (3) zostanie zapisany do komórki pamięci komputera i dlaczego należy się tym przejmować. Są ku temu 4 powody:

  1. Kompilator zoptymalizował kod i postanowił połączyć intrukcje (3) i (4) w jedną instrukcję a+=5; Z punktu widzenia programu w języku C++ jest to równoznaczne. W efekcie, do pamięci może nigdy nie zostać zapisana wartość 2, pojawi się dopiero wartość 5.
  2. Kompilator postanowił zapisać wartość sumy z instrukcji (3) do rejestru procesora. Pozwoli to na zoptymalizowanie wykonania kolejnych instrukcji na zmiennej a. W końcu zapis i odczyt z rejestru jest znacznie szybszy niż odwołania do pamięci. W przedstawionym przypadku zapis do pamięci może się odbyć po wykonaniu instrukcji (3) i (4) jako skopiowanie wyniku z rejestru procesora do pamięci. Podobnie jak w pkt 1. w komórce pamięci może nastąpić zmiana bezpośrednio z wartości 0 na 5.
  3. „Kompilator wysyła” do pamięci wynik wykonania instrukcji (3) czy to poprzez instrukcję add operującą bezpośrednio na pamięci czy to poprzez sekwencję instrukcji add na rejestrze i move czyli przeniesienia wartości z rejestru do pamięci. Problem w tym że te operacje w rzeczywistości nie powodują bezpośredniego zapisu do pamięci komputera. Procesor także posiada mechanizmy, które mają zwiększyć szybkość wykonywania programów. W rzeczywistości nowa wartość trafia najpierw do „write buffer” gdzie czeka na przesłanie dalej, podczas gdy procesor zajmuje się już wykonaniem kolejnych instrukcji. Write buffer to bardzo mały bufor pamięci. Jak tylko jest możliwość przesłania danych do cache procesora opuszczają one write buffer.
  4. Pamięć procesora jest przeważnie znacznie wolniejsza niż sam procesor a samo przesłanie danych wiąże się z dość dużymi opóźnieniami. Z tego powodu procesor został wyposażony w zintegrowaną pamięć zwaną pamięcią cache. Ostatnio używane dane są buforowane w tej pamięci przez co znacznie skraca się ponowny czas dostępu do nich. Również z powodu optymalizacji nowe dane najpierw zostają zapisane do pamięci cache, a dopiero gdy jest taka potrzebna trafiają do pamięci RAM komputera. Na szczęście nie musimy się martwić jak to dokładnie działa. Synchronizacja pamięci cache odbywa się w całości na poziomie sprzętowym, według protokołów MESI i MOESI (w zależności od procesora). Jeśli procesor potrzebuje dane, które znajdują się w pamięci cache innego procesora to zostaną przesłane do tego pierwszego bez żadnej programowej ingerencji.

O ile o optymalizacjach kompilatora i istnieniu pamięci cache procesora wiedzą chyba prawie wszyscy programiści o tyle niewiele osób zdaje sobie sprawę z istnienia „write bruffera”. Mam takie wrażenie przeglądając kody źródłowe niektórych programów i czytając artykuły o programowaniu wielowątkowym w C++. Zwracam szczególną uwagę na to że pamięć cache to co innego niż write buffer, to pierwsze jest synchronizowane sprzętowo, to drugie jest synchronizowane programowo.

Wracając do tytułu posta. Każdy z wątków wykonywanych w ramach jednego procesu może inaczej widzieć w danym momencie pamięć komputera. Różnice wychodzą właśnie z tymczasowych wartości, które znajdują się w rejestrach czy write bufferze. Należy oczywiście zwrócić uwagę na fakt że stan pamięci cache nie wpływa na widok wątku. Jak już wcześniej napisałem synchronizacja pamięci cache odbywa się w całości sprzętowo niewidocznie dla programisty.

Dopóki korzystamy z funkcji pokroju EnterCryticalSection, InterlockedIncrement nie musimy w ogóle zdawać sobie sprawy z tego o czym tutaj napisałem. Jeśli jednak chcemy zrozumieć jak te funkcje działają, chcemy napisać podobne funkcje albo po prostu chcemy nasz program zoptymalizować warto rozumieć te mechanizmy. W następnych postach będę wielokrotnie odwoływał się do tego artykułu, żeby wyjaśnić kiedy pewne optymalizacje są poprawne a kiedy nie i dlaczego.

Leave a Reply