Dlaczego przypisanie liczby całkowitej na zmiennej naturalnie wyrównanej atomic na x86?

Czytałem ten Artykuł o operacjach atomowych, i wspomina, że 32-bitowe przypisanie liczby całkowitej jest atomowe na x86, o ile zmienna jest naturalnie wyrównana.

Dlaczego naturalne wyrównanie zapewnia atomiczność?

Author: Peter Cordes, 2016-04-14

5 answers

"naturalne" wyrównanie oznacza wyrównanie do własnej szerokości typu . W związku z tym load/store nigdy nie zostanie podzielony na jakiekolwiek granice szersze od siebie (np. strona, linia pamięci podręcznej lub nawet węższy rozmiar fragmentu używany do przesyłania danych między różnymi pamięciami podręcznymi).

Procesory często robią takie rzeczy, jak dostęp do pamięci podręcznej lub transfer linii pamięci podręcznej między rdzeniami, w kawałkach wielkości 2, więc granice wyrównania mniejsze niż linia pamięci podręcznej mają znaczenie. (Zobacz komentarze @BeeOnRope poniżej). Atomicity na x86aby uzyskać więcej szczegółów na temat tego, jak procesory implementują ładunki atomowe lub przechowują je wewnętrznie, i czy num++ może być atomowe dla 'int num'? aby dowiedzieć się więcej o tym, jak działają atomowe RMW atomic<int>::fetch_add() / lock xadd są realizowane wewnętrznie.


Po pierwsze, zakłada się, że int jest aktualizowana za pomocą pojedynczej instrukcji store, zamiast zapisywać różne bajty osobno. Jest to część tego, co std::atomic gwarantuje, ale to zwykłe C lub c++ nie. będzie normalnie być ale sprawa. x86-64 System V ABI nie zabrania kompilatorom uzyskiwania dostępu do zmiennych int niematomicznych, nawet jeśli wymaga int, aby być 4B z domyślnym wyrównaniem 4B. na przykład, x = a<<16 | b może skompilować się do dwóch oddzielnych 16-bitowych magazynów, jeśli kompilator tego chce.

Wyścigi danych są niezdefiniowanymi zachowaniami zarówno w C jak i C++, więc Kompilatory mogą i zakładają, że pamięć nie jest modyfikowana asynchronicznie. dla kodu, który nie zostanie złamany, użyj C11 stdatomic lub C++11 std:: atomic . W przeciwnym razie kompilator zachowa wartość w rejestrze zamiast przeładowywać za każdym razem, gdy ją przeczytasz, podobnie jak volatile, ale z rzeczywistymi gwarancjami i oficjalnym wsparciem ze strony standardu językowego.

Przed C++11, atomic ops były zwykle wykonywane z volatile lub innymi rzeczami, a zdrowa dawka "prac nad kompilatorami, na których nam zależy", więc C++11 był ogromnym krokiem naprzód. Teraz już nie musisz przejmować się tym, jaki kompilator does for plain int; wystarczy użyć atomic<int>. Jeśli znajdziesz Stare Przewodniki mówiące o atomiczności int, prawdopodobnie sprzed C++11.

std::atomic<int> shared;  // shared variable (compiler ensures alignment)

int x;           // local variable (compiler can keep it in a register)
x = shared.load(std::memory_order_relaxed);
shared.store(x, std::memory_order_relaxed);
// shared = x;  // don't do that unless you actually need seq_cst, because MFENCE or XCHG is much slower than a simple store

Uwaga: dla atomic<T> większego niż CPU może zrobić atomicznie (więc .is_lock_free() jest false), zobacz gdzie jest blokada dla std::atomic?. int oraz int64_t / uint64_t są jednak wolne od blokad we wszystkich głównych kompilatorach x86.


Tak więc, musimy tylko porozmawiać o zachowaniu insn jak mov [shared], eax.


TL; DR: x86 ISA gwarantuje, że naturalnie wyrównane zapasy i obciążenia są atomowe, o szerokości do 64 bitów. więc Kompilatory mogą używać zwykłych magazynów/ładunków, o ile zapewnią, że std::atomic<T> Ma naturalne wyrównanie.

(ale zauważ, że i386 gcc -m32 Nie robi tego dla 64-bitowych typów C11 _Atomic, tylko wyrównując je do 4B, więc atomic_llong nie jest w rzeczywistości atomowa. https://gcc.gnu.org/bugzilla/show_bug.cgi?id=65146#c4). g++ -m32 Z std::atomic jest w porządku, przynajmniej w G++5, ponieważ https://gcc.gnu.org/bugzilla/show_bug.cgi?id=65147 został poprawiony w 2015 roku poprzez zmianę nagłówka <atomic>. Nie zmieniło to jednak zachowania C11.)


IIRC, istniały systemy SMP 386, ale obecna semantyka pamięci została ustalona dopiero w roku 486. Dlatego w instrukcji jest napisane "486 i nowsze".

[78]}Z "Intel® 64 i IA-32 Architectures Software Developer Manuals, volume 3", z moje notatki kursywą . (Zobacz też tag x86 wiki dla linków: aktualne wersje wszystkich tomów, lub bezpośredni link do strona 256 vol3 pdf z Dec 2015)

W terminologii x86 "słowo" to dwa 8-bitowe bajty. 32 bity to podwójne słowo lub DWORD.

Sekcja 8.1.1 Gwarantowane Operacje Atomowe

Procesor Intel486 (i nowsze procesory od tego czasu) gwarantuje, że następujące podstawowe pamięci operacje będą zawsze wykonywane atomicznie:

  • Czytanie lub zapisanie bajtu
  • [146]}Odczyt lub zapis słowa wyrównanego do 16-bitowej granicy
  • Odczyt lub zapis podwójnego słowa wyrównanego na granicy 32-bitowej (jest to inny sposób na powiedzenie "naturalne wyrównanie")

Ten ostatni punkt, który pogrubiłem, jest odpowiedzią na twoje pytanie: to zachowanie jest częścią tego, co jest wymagane, aby procesor był procesorem x86 (tj. implementacją ISA).


Reszta sekcji zapewnia dalsze gwarancje dla nowszych procesorów Intela: Pentium rozszerza tę gwarancję do 64 bitów .

The Procesor Pentium (i nowsze procesory od tego czasu) gwarantuje, że następujące dodatkowe operacje pamięci będą zawsze wykonywane atomicznie:

  • W 1999 roku, po raz pierwszy w Polsce, wprowadzono do użytku nową wersję języka angielskiego.]} W Pentium P5 procesor X87 jest wyposażony w procesor X87, który jest kompatybilny z procesorem X87.]}
  • 16-bit umożliwia dostęp do lokalizacji pamięci bez pamięci mieszczących się w 32-bitowej szynie danych.
[78]} sekcja dalej wskazuje, że dostępy podzielone na linie pamięci podręcznej (i granice stron) nie są gwarantowane jako atomowe i: [81]}

" Instrukcja x87 lub Instrukcja SSE, która uzyskuje dostęp do danych większych niż quadword, może być zaimplementowana za pomocą dostęp do wielu pamięci."


Instrukcja AMD zgadza się z instrukcją Intela o 64-bitowym i węższym ładunki / sklepy są atomowe

Więc integer, x87 i MMX/SSE ładuje / przechowuje do 64B, nawet w trybie 32-bitowym lub 16-bitowym (np. movq, movsd, movhps, pinsrq, extractps, etc) atomowe, jeśli dane są wyrównane. gcc -m32 używa movq xmm, [mem] do implementacji atomowych 64-bitowych obciążeń dla takich rzeczy jak std::atomic<int64_t>. Clang4. 0 -m32 niestety używa lock cmpxchg8b bug 33109 .

Na niektórych procesorach z wewnętrznymi ścieżkami danych 128b lub 256b (między jednostkami wykonawczymi i L1 oraz między różnymi pamięciami podręcznymi), 128b i nawet ładunki wektorowe 256b są atomowe, ale nie jest to gwarantowane przez jakikolwiek standard lub łatwe do zapytania w czasie wykonywania, niestety dla kompilatorów implementujących std::atomic<__int128> lub struktury 16B.

Jeśli chcesz mieć atomic 128B we wszystkich systemach x86, musisz użyć lock cmpxchg16b (dostępne tylko w trybie 64-bitowym). (I nie był dostępny w procesorach pierwszej generacji x86-64. Trzeba użyć -mcx16 z gcc/clang aby je emitować.)

Nawet procesory, które wewnętrznie robią atomic 128B loads/stores może wykazywać nieatomiczne zachowanie w systemach z wieloma gniazdami z protokołem koherencyjnym, który działa w mniejszych kawałkach: np. AMD Opteron 2435 (K10) z wątkami działającymi na oddzielnych gniazdach, połączonymi z HyperTransport.


W przeciwieństwie do innych komputerów Intel i AMD nie mają dostępu do pamięci podręcznej . Wspólnym podzbiorem dla wszystkich procesorów x86 jest reguła AMD. Cacheable oznacza regiony pamięci odpisu lub zapisu, a nie uncacheable or write-combining, as set with PAT or MTRR regions. Nie oznacza to, że linia cache musi być już gorąca w buforze L1.

  • Intel P6 i Później gwarantują atomiczność dla cacheable ładuje / przechowuje do 64 bitów, o ile są one w jednej linii pamięci podręcznej (64B lub 32B na bardzo starych procesorach, takich jak PentiumIII).
  • AMD gwarantuje atomicity dla cachowalnych obciążeń/magazynów, które mieszczą się w pojedynczym fragmencie 8B wyrównanym. To ma sens, bo wiemy z 16B - test store na multi-socket Opteron, który HyperTransport przenosi tylko w kawałkach 8B i nie blokuje się podczas przenoszenia, aby zapobiec rozdarciu. (Patrz wyżej). Myślę, że lock cmpxchg16b musi być obsługiwany specjalnie.

    Prawdopodobnie powiązane: AMD używa MOESI do współdzielenia brudnych linii pamięci podręcznej bezpośrednio między pamięciami podręcznymi w różnych rdzeniach, więc jeden rdzeń może odczytywać z poprawnej kopii linii pamięci podręcznej, podczas gdy aktualizacje do niej przychodzą z innej pamięci podręcznej.

    Intel używa MESIF , co wymaga, aby brudne dane rozprzestrzeniały się do dużej współdzielonej pamięci podręcznej L3, która działa jako zabezpieczenie dla ruchu spójnego. L3 zawiera znaczniki pamięci podręcznej L2/L1 na rdzeń, nawet dla linii, które muszą być w nieprawidłowym stanie w L3, ponieważ są M lub E w pamięci podręcznej L1 na rdzeń. Ścieżka danych między L3 i cache per-core jest tylko 32B szerokości w Haswell / Skylake, więc musi bufor lub coś, aby uniknąć zapisu do L3 z jednego rdzenia dzieje się między odczytami dwóch połówek linii cache, które może spowodować rozerwanie granicy 32B.

Odpowiednie sekcje podręczników:

Procesory z rodziny P6 (oraz nowsze procesory Intel ponieważ) gwarantują, że następujące dodatkowe operacje pamięci będą zawsze być wykonywane atomicznie:

  • niepodpisany 16-, 32-i 64-bitowy dostęp do pamięci podręcznej mieszczącej się w linii pamięci podręcznej.

AMD64 Manual 7.3.2 Dostęp Do Atomicity
Cachowalne, naturalnie wyrównane pojedyncze ładunki lub zapasy do quadword są atomowe na każdym procesorze model, podobnie jak niewspółosiowe ładunki lub magazyny o wartości mniejszej niż czworokąt, które są zawarte całkowicie w naturalnie wyrównanym quadword

Zauważ, że AMD gwarantuje atomiczność dla każdego obciążenia mniejszego niż qword, ale Intel tylko dla wielkości mocy 2. 32-bitowy tryb chroniony i 64-bitowy tryb długi mogą ładować 48-bitowy m16:32 jako operand pamięci do cs:eip z daleko-call lub daleko - jmp. (I daleko-call popycha rzeczy na stosie.) IDK, jeśli liczy się to jako pojedynczy dostęp 48-bitowy lub oddzielne 16-i 32-bitowe.

Były próby sformalizowania modelu pamięci x86, ostatnią z nich jest artykuł x86-tso (extended version) z 2009 (link z sekcji porządkowania pamięci na x86 {84]} tag wiki). Nie jest to użyteczne, ponieważ definiują niektóre symbole, aby wyrazić rzeczy we własnej notacji, a ja nie próbowałem tego przeczytać. IDK, jeśli opisuje reguły atomiczności, lub jeśli dotyczy tylko pamięci .


Atomic Read-Modify-Write

Wspomniałem cmpxchg8b, ale mówiłem tylko o ładunku i sklepie, każdy z osobna jest atomowy (tzn. nie ma "Rozdarcia", gdzie połowa ładunku jest z jednego sklepu, druga połowa ładunku jest z innego sklepu).

Aby zapobiec modyfikowaniu zawartości tego miejsca pamięci pomiędzy ładunkiem a sklepem, potrzebujesz lock cmpxchg8b, tak jak potrzebujesz lock inc [mem], aby cały odczyt-modyfikacja-zapis był atomowy. Zauważ również, że nawet jeśli cmpxchg8b BEZ lock wykonuje pojedyncze obciążenie atomowe( i opcjonalnie magazyn), nie jest bezpiecznie używać go jako obciążenia 64B z expected=desired. Jeśli wartość w pamięci będzie pasować do oczekiwanych, otrzymasz nieatomiczny odczyt-modyfikację-zapis tej lokalizacji.

Prefiks lock sprawia, że nawet bez umożliwia dostęp do atomowych granic linii pamięci podręcznej lub strony, ale nie można jej użyć z mov, aby utworzyć niepodpisany Store lub load atomic. Można go używać tylko z instrukcjami read-modify-write memory-destination, takimi jak add [mem], eax.

(lock jest niejawne w xchg reg, [mem], więc nie używaj xchg Z mem, aby zapisać rozmiar kodu lub liczbę instrukcji, chyba że wydajność jest nieistotna. Używaj go tylko wtedy, gdy chcesz bariery pamięci i / lub wymiany atomowej, lub gdy rozmiar kodu jest jedyną rzeczą, która sprawy, np. w sektorze rozruchowym.)

Zobacz także: czy num++ może być atomowe dla 'int num'?


Dlaczego lock mov [mem], reg nie istnieje dla atomic unaligned stores

Z podręcznika insn ref (Intel x86 manual vol2), cmpxchg:

Ta instrukcja może być używana z prefiksem LOCK, aby umożliwić instrukcja do wykonania atomicznie. Aby uprościć interfejs do magistrali procesora, operand docelowy otrzymuje cykl zapisu bez względu na wynik porównania. Miejsce przeznaczenia operand jest odpisywany, jeśli porównanie się nie powiedzie; w przeciwnym razie źródło operand jest zapisywany do miejsca docelowego. (procesor nigdy nie produkuje zablokowany odczyt bez generowania również zablokowanego zapisu .)

[78]}ta decyzja projektowa zmniejszyła złożoność chipsetu, zanim kontroler pamięci został wbudowany w procesor. Nadal może tak być w przypadku lockinstrukcji ed na regionach MMIO, które uderzają w magistralę PCI-express, a nie w pamięć DRAM. Informatyka po prostu byłoby mylące dla lock mov reg, [MMIO_PORT], aby wytworzyć zarówno zapis, jak i odczyt do rejestru We/Wy mapowanego w pamięci.

Innym wyjaśnieniem jest to, że nie jest bardzo trudne, aby upewnić się, że Twoje dane mają naturalne wyrównanie, i {63]} będzie działać okropnie w porównaniu do tylko upewniając się, że Twoje dane są wyrównane. Głupio byłoby wydawać Tranzystory na coś, co byłoby tak powolne, że nie byłoby warte użycia. Jeśli naprawdę tego potrzebujesz (i nie masz nic przeciwko czytaniu pamięci), możesz użyć xchg [mem], reg (XCHG ma prefiks blokady implicit), który jest nawet wolniejszy od hipotetycznego lock mov.

Użycie prefiksu lock jest również pełną barierą pamięci, więc narzuca narzut wydajności wykraczający poza tylko atomowy RMW. tzn. x86 nie potrafi zrobić rozluźnionego atomic RMW (bez spłukiwania bufora sklepu). Inne Isa mogą, więc użycie .fetch_add(1, memory_order_relaxed) może być szybsze na nie-x86.

Ciekawostka: przed mfence istniał wspólny idiom lock add dword [esp], 0, który jest nie-op innym niż trzepanie FLAG i wykonywanie operacji zablokowanych. [esp] jest prawie zawsze gorący w pamięci podręcznej L1 i nie powoduje konfliktu z żadnym innym rdzeniem. Ten idiom może być jeszcze bardziej wydajny niż MFENCE jako samodzielna bariera pamięci, zwłaszcza w procesorach AMD.

xchg [mem], reg jest prawdopodobnie najskuteczniejszym sposobem wdrożenia sklepu o konsystencji sekwencyjnej, vs.mov+mfence, zarówno na Intel jak i AMD. mfence na Skylake przynajmniej blokuje wykonanie instrukcji Nie-pamięci, ale xchgi inne lock ed ops Nie. Kompilatory inne niż gcc używa xchg dla sklepów, nawet jeśli nie zależy im na odczytaniu starej wartości.


Motywacja do tej decyzji projektowej:

Bez niego, oprogramowanie musiałoby używać 1-bajtowych blokad (lub jakiegoś dostępnego typu atomowego) do ochrony dostępu do 32-bitowych liczb całkowitych, co jest ogromnie nieefektywne w porównaniu do współdzielonego atomic read access dla czegoś takiego jak globalna zmienna znacznika czasu aktualizowana przez przerwanie timera. Prawdopodobnie jest w zasadzie darmowy w krzemie, aby zagwarantować wyrównane wejścia o szerokości magistrali lub mniejsze.

Aby blokada była w ogóle możliwa, potrzebny jest jakiś dostęp atomowy. (Właściwie, myślę, że sprzęt może zapewnić jakiś zupełnie inny sprzętowo wspomagany mechanizm blokujący.) Dla PROCESORA, który wykonuje 32-bitowe transfery na swojej zewnętrznej magistrali danych, po prostu ma sens, aby była to jednostka atomiczności.


Skoro zaoferowałeś nagrodę, zakładam, że szukałeś długiej odpowiedzi, która zawędrowała do wszystkich interesujących tematy poboczne. Daj mi znać, jeśli są rzeczy, których nie omówiłem, które Twoim zdaniem uczynią to pytanie i odpowiedź bardziej wartościową dla przyszłych czytelników.

Od kiedy podlinkowałeś jedną w pytaniu, bardzo polecam przeczytać więcej wpisów na blogu Jeffa Preshinga . Są doskonałe i pomogły mi połączyć elementy tego, co wiedziałem w zrozumieniu porządkowania pamięci w C/C++ source vs. asm dla różnych architektur sprzętowych, i jak / kiedy powiedzieć kompilatorowi, co chcesz, jeśli nie piszesz bezpośrednio asm.

 23
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-20 00:15:30

Jeśli 32-bitowy lub mniejszy obiekt jest naturalnie wyrównany w "normalnej" części pamięci, będzie to możliwe dla dowolnego procesora 80386 lub kompatybilnego innego niż 80386sx do odczytu lub zapisu wszystkich 32 bitów obiektu w ramach jednej operacji. Chociaż zdolność platformy do zrobienia czegoś w szybki i użyteczny sposób nie musi oznaczać, że platforma nie będzie czasami robić tego w inny sposób z jakiegoś powodu, i chociaż wierzę, że jest to możliwe na wielu, jeśli nie wszystkich procesorach x86, aby mieć regiony pamięci, do których można uzyskać dostęp tylko 8 lub 16 bitów na raz, nie sądzę, aby Intel kiedykolwiek zdefiniował jakiekolwiek warunki, w których żądanie wyrównanego 32-bitowego dostępu do "normalnego" obszaru pamięci spowodowałoby, że system odczyta lub zapisze część wartości bez czytania lub pisania całości, i nie sądzę, aby Intel kiedykolwiek zdefiniował coś takiego dla "normalnych" obszarów pamięci.

 6
Author: supercat,
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
2016-04-17 18:23:34

Naturalnie wyrównany oznacza, że adres typu jest wielokrotnością rozmiaru typu.

Na przykład bajt może być pod dowolnym adresem, krótki (przy założeniu 16 bitów) musi być na wielokrotności 2, int (przy założeniu 32 bitów) musi być na wielokrotności 4, A długi (przy założeniu 64 bitów) musi być na wielokrotności 8.

W przypadku, gdy uzyskasz dostęp do danych, które nie są naturalnie wyrównane, procesor albo wywoła błąd, albo będzie odczytywał / zapisywał pamięć, ale nie jako atomic operacja. Działanie procesora zależy od architektury.

Na przykład, mamy układ pamięci poniżej:

01234567
...XXXX.

I

int *data = (int*)3;

Kiedy próbujemy odczytać *data bajty składające się na wartość są rozłożone na 2 bloki int size, 1 bajt jest w bloku 0-3, a 3 bajty w bloku 4-7. To, że bloki są logicznie obok siebie, nie oznacza, że są fizycznie. Na przykład blok 0-3 może znajdować się na końcu linii pamięci podręcznej procesora, podczas gdy blok 3-7 znajduje się w pliku strony. Gdy procesor przechodzi do bloku dostępu 3-7, aby uzyskać potrzebne 3 bajty, może zobaczyć, że blok nie jest w pamięci i sygnalizuje, że potrzebuje pamięci paged. Prawdopodobnie zablokuje to proces wywołania, podczas gdy system operacyjny ponownie włącza pamięć.

Po zapisaniu pamięci, ale zanim twój proces się obudzi, może przyjść inny i napisać Y na adres 4. Następnie Proces jest przesunięty, a procesor kończy odczyt, ale teraz przeczytał XYXX, a nie XXXX, którego się spodziewałeś.

 2
Author: Sean,
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
2016-04-14 15:03:16

Gdybyś pytał, dlaczego jest tak zaprojektowany, powiedziałbym, że jest to dobry produkt poboczny z projektowania architektury CPU.

W czasach 486 nie ma wielordzeniowego procesora ani łącza QPI, więc atomicity nie jest w tym czasie ściśle wymaganym wymogiem (DMA może tego wymagać?).

Na x86 szerokość danych wynosi 32 bity( lub 64 Bity dla x86_64), co oznacza, że procesor może odczytywać i zapisywać dane do szerokości w jednym ujęciu. Szyna danych pamięci jest zwykle taka sama lub szersza od tej liczby. W połączeniu z fakt, że odczyt/zapis na wyrównanym adresie odbywa się w jednym ujęciu, oczywiście nic nie stoi na przeszkodzie, aby odczyt/zapis był nieatomiczny. Zyskujesz prędkość / atomic w tym samym czasie.

 1
Author: Wei Shen,
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
2016-04-19 20:28:18

Aby odpowiedzieć na pierwsze pytanie, zmienna jest naturalnie wyrównana, jeśli istnieje pod adresem pamięci, który jest wielokrotnością jej rozmiaru.

Jeśli weźmiemy pod uwagę tylko - tak jak artykuł, który podlinkowałeś - Instrukcje przypisania , to wyrównanie gwarantuje atomiczność, ponieważ MOV (Instrukcja przypisania) jest atomowa z założenia na wyrównanych danych.

Inne rodzaje instrukcji, np. INC, muszą być LOCK ed (prefiks x86 dający wyłączny dostęp do udostępnionego pamięci do bieżącego procesora na czas operacji poprzedzonej), nawet jeśli dane są wyrównane, ponieważ faktycznie wykonują wiele kroków (=instrukcje, mianowicie load, inc, store).

 0
Author: Francis Straccia,
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
2016-04-14 14:29:48