2013-05-23 11 views
12

Miałem wrażenie, że ładowanie pamięci nie może zostać podniesione ponad ładunek przejęty w modelu pamięci C++ 11. Jednak patrząc na kod, który produkuje gcc 4.8, wydaje się, że tylko w przypadku innych ładunków atomowych, a nie całej pamięci. Jeśli to prawda, a ładowanie odbiorów nie synchronizuje całej pamięci (tylko std::atomics), to nie jestem pewny, jak byłoby możliwe zastosowanie muteksów ogólnego przeznaczenia w kategoriach std :: atomic.Podnoszenie ładunków nieatomowych poprzez przejęcie obciążeń atomowych

Poniższy kod:

extern std::atomic<unsigned> seq; 
extern std::atomic<int> data; 

int reader() { 
    int data_copy; 
    unsigned seq0; 
    unsigned seq1; 
    do { 
     seq0 = seq.load(std::memory_order_acquire); 
     data_copy = data.load(std::memory_order_relaxed); 
     std::atomic_thread_fence(std::memory_order_acquire); 
     seq1 = seq.load(std::memory_order_relaxed); 
    } while (seq0 != seq1); 
    return data_copy; 
} 

Produkuje:

_Z6readerv: 
.L3: 
    mov ecx, DWORD PTR seq[rip] 
    mov eax, DWORD PTR data[rip] 
    mov edx, DWORD PTR seq[rip] 
    cmp ecx, edx 
    jne .L3 
    rep ret 

który wygląda poprawna do mnie.

jednak zmianę danych być int zamiast std::atomic:

extern std::atomic<unsigned> seq; 
extern int data; 

int reader() { 
    int data_copy; 
    unsigned seq0; 
    unsigned seq1; 
    do { 
     seq0 = seq.load(std::memory_order_acquire); 
     data_copy = data; 
     std::atomic_thread_fence(std::memory_order_acquire); 
     seq1 = seq.load(std::memory_order_relaxed); 
    } while (seq0 != seq1); 
    return data_copy; 
} 

Wytwarza to:

_Z6readerv: 
    mov eax, DWORD PTR data[rip] 
.L3: 
    mov ecx, DWORD PTR seq[rip] 
    mov edx, DWORD PTR seq[rip] 
    cmp ecx, edx 
    jne .L3 
    rep ret 

Więc co się dzieje?

+0

Jeśli przepisujesz kolejność atomów na 'load (rel); fence (acq); 'w drugiej wersji, czy zmienia się jej wyjście asm? – yohjp

+0

@yoyjp Czy odnosisz się do ładowania 'seq0'? Jeśli tak, to nie, nie ma to wpływu na generowany w ogóle kod. – jleahy

+0

Nie, wspomniałem "seq1". "Przejęcie ogrodzenia", które uzyskało semantykę, składa się z 'seq1.load (relaxed) -> fence (nabycie)' ops order, a nie 'fence (acquire) -> seq1.load (relaxed)' w pamięci C++ 11 Model. "Ogrodzenie" C++ ** tylko ** wpływa na _happens-before relationship_ pomiędzy operacjami atomowymi i/lub płotami, ma ** ** wpływ bezpośredni na nieatomowe vary. W tym miejscu "ogrodzenie" C++ różni się znacznie od instrukcji zapory pamięciowej procesora/kompilatora (np. Mfence x86). – yohjp

Odpowiedz

4

Dlaczego obciążenie została podniesiona o ponad nabywać

ja napisałem to na gcc bugzilla a oni potwierdził to jako błąd.

MEM alias-set od -1 (ALIAS_SET_MEMORY_BARRIER) ma zapobiec, ale PRE nie wie o tej szczególnej właściwości (należy "zabić" wszyscy sędziowie przekraczania go).

To wygląda gcc wiki ma ładny strony na ten temat.

Zasadniczo, komunikat jest barierą dla kodu zatopienie, nabycie bariera kodowi podnoszenia.

Dlaczego ten kod jest nadal uszkodzony

Zgodnie this paper mojego kodu jest nadal nieprawidłowa, ponieważ wprowadza wyścig danych. Mimo że poprawiony gcc generuje poprawny kod, nadal nie jest odpowiedni dostęp do data bez zawijania go w std::atomic.Powodem jest to, że wyścigi danych są niezdefiniowanym zachowaniem, nawet jeśli obliczenia z nich wynikające są odrzucane.

Przykładem uprzejmości AdamH.Peterson:

int foo(unsigned x) { 
    if (x < 10) { 
     /* some calculations that spill all the 
      registers so x has to be reloaded below */ 
     switch (x) { 
     case 0: 
      return 5; 
     case 1: 
      return 10; 
     // ... 
     case 9: 
      return 43; 
     } 
    } 
    return 0; 
} 

Tutaj kompilator może zoptymalizować przełącznik do tabeli skoku, a dzięki if powyżej będzie w stanie uniknąć Sprawdzić zakres. Jeśli jednak wyścigi danych nie byłyby niezdefiniowane, wymagana byłaby kontrola drugiego zakresu.

+4

Te 2 nie są niekompatybilne. Twój kod ma wyścig danych, a standard C++ jasno mówi (1.10 21), że twój kod opiera się na niezdefiniowanym zachowaniu. Kod jest niepoprawny (lub przynajmniej brakuje prawidłowej synchronizacji, aby udowodnić swój punkt). Po raz kolejny papier hp również to wyjaśnia (autor jest jednym z architektów modelu pamięci C++ 11) 1.10 13 mówi, że gcc nie może wykonywać wciągania kodu. Jeśli dzieje się to przy prawidłowym kodzie C++, jest to błąd. Cały punkt był taki, że * jeśli * nie było wyścigu danych, wygenerowany kod byłby poprawny (przynajmniej nie rozumiem, dlaczego nie) – Guillaume

+4

@GuillaumeMorin: Mam ochotę się z tym zgodzić. Sekwencja zwolnienia, która jest tworzona przez magazyn i obciążenie, jest tylko jednym elementem łańcucha zdarzeń. Kod byłby poprawny, gdy drugi wątek zostałby opublikowany, gdyby pierwszy wątek zawierał coś w stylu: "if (seq_copy == 2) {data_copy = data; } '. W takim przypadku 'data = 2' * dzieje się - przed * magazynem atomowym, który * synchronizuje się - z * ładunkiem atomowym, który * zachodzi - przed * danymi' data_kopia = dane'. Z opublikowanym kodem dostęp do "danych" powoduje wyścig. (Poprawiony kod również generuje prawidłowy wynik). –

+0

@GuillaumeMorin Zdaję sobie sprawę, że masz teraz rację, poprawiłem swoją odpowiedź, aby pomóc komukolwiek, kto to widzi. Szkoda, że ​​wody zostały zmącone przez fakt, że był też błąd w gcc. – jleahy

0

Nadal nie mam pojęcia o tych niekonsekwencyjnie sekwencyjnych operacjach i barierach związanych z kolejnością pamięci, ale możliwe, że generowanie tego kodu jest poprawne (lub raczej dopuszczalne). Na pierwszy rzut oka wygląda to na podejrzanie, ale nie zdziwiłbym się, gdyby program w standardzie nie był w stanie stwierdzić, że ładunek z danych został podniesiony (co oznaczałoby, że kod ten jest poprawny pod "jak gdyby"). "zasada).

Program odczytuje dwie kolejne wartości z bloku atomowego, jeden przed ładowaniem i jeden po załadowaniu, a następnie ponownie ładuje, gdy nie pasują. Zasadniczo nie ma powodu, dla którego dwa odczyty atomowe będą musiały od widzieć różne wartości od siebie. Nawet jeśli właśnie nastąpił zapis atomowy, nie ma możliwości, aby ten wątek mógł wykryć, że ponownie nie odczytał starej wartości. Wątek następnie powróciłby do pętli i ostatecznie odczytałby dwie stałe wartości z atomu, a następnie powrócił, ale ponieważ seq0 i są następnie odrzucane, program nie może stwierdzić, że wartość w seq0 nie odpowiada wartości odczytanej od data. Teraz w zasadzie sugeruje mi to również, że cała pętla mogła zostać usunięta i tylko ładunek z data jest rzeczywiście potrzebny do poprawności, ale niepowodzenie w wyprowadzeniu pętli niekoniecznie jest problemem poprawności.

Jeśli reader() były zwrócić pair<int,unsigned> które obejmowały seq0 (lub seq1) i ten sam podniósł pętli zostały wygenerowane, myślę, że to prawdopodobnie niepoprawny kod (ale znowu jestem nowy na tym niesekwencyjnie spójne operacje rozumowania).

+0

Nie jestem pewien, czy masz rację. Jeśli to, co mówisz, jest prawdą, to ta kombinacja barier nie wystarcza do wdrożenia seq-lock, co jest sprzeczne z tym, co HP napisał w tym dokumencie: http://www.hpl.hp.com/techreports/ 2012/HPL-2012-68.pdf. Co więcej, nadal oczekiwałbyś, że kompilator wygeneruje ten sam kod dla obu wejść (i prawdopodobnie po prostu usunie całą pętlę, tak jak bez żadnych barier). – jleahy

+0

@jleahy, myślę, że ten dokument nie jest całkiem konkretny. Po pierwsze, w dokumencie wszystkie udostępnione odczyty i zapisy są wykonywane na rzeczywistych 'atomowych' (z wyjątkiem przykładów, które wskazują, że są niepoprawne), ponieważ zwykłe zmienne podlegają ograniczeniom dotyczącym danych rasowych (brak konfliktowych dostępów) w celu uniknięcia nieokreślonego zachowania. Po drugie, pętle wykonywane w celu oznaczenia odczytów i zapisów faktycznie wykonują pewne nietrywialne obliczenia wartości sekwencji odczytu w celu weryfikacji spójności, co nie ma zastosowania w twoim przykładzie. IME, każdy z tych punktów jest wystarczający, aby wyraźnie odróżnić kod od optymalizatora. –

1

Nie sądzę, aby wartość atomic_thread_fence była poprawna. Jedynym modelem pamięci C++ 11, który współpracuje z twoim kodem, byłby seq_cst jeden. Ale to jest bardzo drogie (masz zamiar zdobyć pełne ogrodzenie pamięci) na to, czego potrzebujesz.

Oryginalny kod działa i myślę, że to najlepsza kompromitacja wydajności.

EDIT oparciu o aktualizacjach:

Jeśli szukasz formalny powód kod z regularnym int nie działa tak, że Ci się podoba, uważam, że bardzo Cię papier cytowany (http://www.hpl.hp.com/techreports/2012/HPL-2012-68.pdf) daje odpowiedź. Spójrz na koniec sekcji 2. Twój kod ma ten sam problem, co kod na ryc. 1. Ma wyścigi danych. Wiele wątków może wykonywać operacje na tej samej pamięci w regularnych intach w tym samym czasie. Jest to zabronione przez model pamięci C++ 11, ten kod jest formalnie nieprawidłowy kod C++.

gcc oczekuje, że kod nie zawiera wyścigu danych, tj. Jest to poprawny kod C++. Ponieważ nie ma rasy i kod ładuje int bezwarunkowo, ładunek może być emitowany w dowolnym miejscu w ciele funkcji. Więc gcc jest inteligentny i emituje go tylko raz, ponieważ nie jest niestabilny. Warunkowe oświadczenie, które zwykle idzie w parze z przejmującą barierą, odgrywa ważną rolę w tym, co zrobi kompilator.

W formalnym slangu normy, obciążenia atomowe i regularne obciążenia int są niesekwencjonowane. Wprowadzenie na przykład warunku tworzy punkt sekwencji i zmusi kompilator do oceny regularnego int po punkcie sekwencji (http://msdn.microsoft.com/en-us/library/d45c7a5d.aspx). Następnie model pamięciowy C++ wykona resztę (tj. Zapewni widoczność przez procesor wykonujący instrukcje).

Więc żadne z twoich oświadczeń nie jest prawdziwe. Możesz na pewno zbudować blokadę z C++ 11, po prostu nie z wyścigami danych :-) Zazwyczaj blokada wymagałaby czekania przed czytaniem (czego oczywiście próbujesz uniknąć), więc nie masz tego rodzaju problemy.

Należy zauważyć, że oryginalny seqlock jest błędny, ponieważ nie chcesz po prostu sprawdzać seq0! = Seq1 (możesz być w trakcie aktualizacji). Papier dodatkowy ma prawidłowy stan.

+0

Nic z tego nie wynika, dlaczego luźne i nieatomowe ładunki są traktowane inaczej dla zamawiania pamięci. – jleahy

+0

Nie są. To, co dostajesz z std :: atomic, to "zmienność" typu bazowego. W drugim przypadku uzyskasz takie samo zachowanie, jeśli zmienisz swoje 'int dane' na' volatile int data'. Ale to raczej podstęp. Tak naprawdę w duchu C++ 11 można użyć int, ale z odpowiednim modelem pamięci (seq const, bardzo drogie, więc nie polecam go) lub uczynić dane atomowymi z luźnymi ładunkami. – Guillaume

+0

Jestem prawie pewny, że nie potrzebujesz seq const, co równa się mfence na x86 i ten kod działa poprawnie przy użyciu asm volatile i no mfence. (Używałem go miliardy razy). Artykuł, o którym wspominałem w moim drugim komentarzu (hpl.hp.com/techreports/2012/HPL-2012-68.pdf) również stwierdza, że ​​jest to wystarczające ograniczenie. – jleahy

Powiązane problemy