Czy zmienne, ale nieobciążone odczyty mogą dawać wartości w nieskończoność? (na prawdziwym sprzęcie)

Odpowiadając na to pytanie pojawiło się kolejne pytanie o sytuację OP, którego nie byłem pewien: jest to głównie pytanie o architekturę procesora, ale z pytaniem knock-on o model pamięci C++ 11.

Zasadniczo, kod OP był zapętlany nieskończenie na wyższych poziomach optymalizacji ze względu na następujący kod (nieco zmodyfikowany dla uproszczenia):

while (true) {
    uint8_t ov = bits_; // bits_ is some "uint8_t" non-local variable
    if (ov & MASK) {
        continue;
    }
    if (ov == __sync_val_compare_and_swap(&bits_, ov, ov | MASK)) {
        break;
    }
}

Gdzie __sync_val_compare_and_swap() jest wbudowanym atomowym CAS GCC. GCC (rozsądnie) zoptymalizował to do pętla nieskończona w przypadku, gdy bits_ & mask została wykryta jako true przed wejściem do pętli, pomijając całkowicie operację CAS, więc zasugerowałem następującą zmianę (która działa):

while (true) {
    uint8_t ov = bits_; // bits_ is some "uint8_t" non-local variable
    if (ov & MASK) {
        __sync_synchronize();
        continue;
    }
    if (ov == __sync_val_compare_and_swap(&bits_, ov, ov | MASK)) {
        break;
    }
}

Po tym jak odpowiedziałem, OP zauważył, że zmiana bits_ na volatile uint8_t wydaje się również działać. Zasugerowałem, aby nie iść tą drogą, ponieważ volatile nie powinny być normalnie używane do synchronizacji, a nie wydaje się być wiele minusów korzystania z ogrodzenia tutaj i tak.

Jednak myślałem o tym bardziej i w tym przypadku semantyka jest taka, że nie ma znaczenia, czy sprawdzanie ov & MASK opiera się na starej wartości, o ile nie opiera się na nieokreślonej starej wartości (tzn. o ile pętla zostanie ostatecznie przerwana), ponieważ rzeczywista próba aktualizacji bits_ jest zsynchronizowana. Czy więc volatile wystarczy, aby zagwarantować, że ta pętla zakończy się w końcu, jeśli bits_ zostanie zaktualizowana przez inny wątek taki, że bits_ & MASK == false, dla dowolnego istniejącego procesora? Innymi słowy, w przypadku braku wyraźnego zapamiętywania, czy jest to praktycznie możliwe, aby odczyty nie zoptymalizowane przez kompilator były efektywnie optymalizowane przez procesor, a nie w nieskończoność? ( EDIT: aby było jasne, pytam tutaj o to, co nowoczesny sprzęt może faktycznie zrobić, biorąc pod uwagę założenie, że odczyty są emitowane w pętli przez kompilator, więc nie jest to technicznie kwestia języka, chociaż wyrażanie tego w kategoriach semantyki C++ jest wygodne.)

To jest kąt sprzętowy do niego, ale aby go nieco zaktualizować i zrobić to również C++11 jest pamięcią typu C ++ 11, która może być użyta w pamięci C ++ 11, ale nie może być użyta w pamięci C ++ 11.]}

// bits_ is "std::atomic<unsigned char>"
unsigned char ov = bits_.load(std::memory_order_relaxed);
while (true) {
    if (ov & MASK) {
        ov = bits_.load(std::memory_order_relaxed);
        continue;
    }
    // compare_exchange_weak also updates ov if the exchange fails
    if (bits_.compare_exchange_weak(ov, ov | MASK, std::memory_order_acq_rel)) {
        break;
    }
}

Cppreference twierdzi, że std::memory_order_relaxed implikuje "brak ograniczeń dotyczących zmiany kolejności dostępu do pamięci wokół zmiennej atomowej", więc niezależnie od tego, co rzeczywisty sprzęt zrobi lub nie zrobi, oznacza, że bits_.load(std::memory_order_relaxed) może technicznienigdy odczytać zaktualizowaną wartość po bits_ zostanie zaktualizowana w innym wątku. wdrożenie?

EDIT: znalazłem to w standardzie (29.4 p13):

Implementacje powinny sprawić, że magazyny atomowe będą widoczne dla ładunków atomowych w rozsądnym czasie.

Więc widocznie oczekiwanie "nieskończenie długo" na zaktualizowaną wartość jest (głównie?) nie wchodzi w grę, ale nie ma twardej gwarancji, że jakiś konkretny przedział czasu świeżości inny niż ten powinien być "rozsądny"; mimo to pytanie o rzeczywiste zachowanie sprzętu stoi.

Author: Community, 2013-03-18

4 answers

C++11 Atomics deal with three issues:

  1. Zapewnienie, że pełna wartość jest odczytywana lub zapisywana bez przełącznika wątku; zapobiega to rozerwaniu.

  2. Upewnienie się, że kompilator nie porządkuje instrukcji w wątku przez atomowy odczyt lub zapis; zapewnia to porządkowanie w wątku.

  3. Zapewnienie (dla odpowiednich wyborów parametrów kolejności pamięci), że dane zapisane w wątku przed zapisem atomowym będą widoczne przez wątek, który odczytuje zmienną atomową i widzi wartość, która została zapisana. To jest widoczność.

Kiedy używasz memory_order_relaxed nie masz gwarancji widoczności z relaksującego sklepu lub ładunku. Masz dwie pierwsze Gwarancje.

Implementacje "powinny" (tzn. są zachęcane do) uwidocznienia zapisów pamięci w rozsądnym czasie, nawet przy swobodnym zamawianiu. To najlepsze, co można powiedzieć; prędzej czy później te rzeczy powinny pokazać w górę.

Więc, tak, formalnie, implementacja, która nigdy nie sprawiała, że relaxed writes był widoczny dla relaxed reads jest zgodna z definicją języka. W praktyce tak się nie stanie.

O to, co robi volatile, zapytaj sprzedawcę kompilatora. To zależy od realizacji.

 9
Author: Pete Becker,
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
2013-03-17 23:05:42

Jest technicznie legalne dla std::memory_order_relaxed obciążeń, aby nigdy, przenigdy nie zwracać nowej wartości dla obciążenia. Co do tego, czy jakakolwiek implementacja to zrobi, nie mam pojęcia.

Odniesienie: http://www.developerfusion.com/article/138018/memory-ordering-for-atomic-operations-in-c0x/ " jedynym wymogiem jest to, że dostęp do pojedynczej zmiennej atomowej z tego samego wątku nie może być ponownie uporządkowany: gdy dany wątek widział określoną wartość zmiennej atomowej, a następnie odczytany przez to thread nie może pobrać wcześniejszej wartości zmiennej."

 4
Author: Patashu,
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
2013-03-17 23:11:47

Jeśli procesory nie mają protokołu cache-coherence lub mają go bardzo prostego, to może "zoptymalizować" ładunki pobierające stare dane z pamięci podręcznej. Obecnie większość nowoczesnych procesorów wielordzeniowych implementuje protokół koherencji pamięci podręcznej. Jednak ramię przed A9 nie miało go. Architektury inne niż CPU również mogą nie mieć spójności pamięci podręcznej (chociaż prawdopodobnie nie będą zgodne z modelem pamięci C++).

Innym problemem jest to, że wiele architektur (w tym ARM i x86) pozwala na zmianę kolejności dostępu do pamięci. Nie wiem. wiem, czy procesory są wystarczająco inteligentne, aby zauważyć powtarzające się dostępy do tego samego adresu, ale wątpię w to (to kosztuje miejsce i czas dla rzadkiego przypadku, jak kompilator powinien być w stanie to zauważyć, z małymi korzyściami, ponieważ późniejsze dostępy prawdopodobnie będą L1 hits), ale technicznie można spekulować, że branch zostanie zabrany i może zmienić kolejność drugiego dostępu przed pierwszym (mało prawdopodobne, ale jeśli odczytam poprawnie instrukcję Intela i ARM, jest to dozwolone).

Wreszcie są urządzenia zewnętrzne, które nie przylegają do cache-coherency. Jeżeli CPU komunikuje się poprzez mapowanie pamięci IO / DMA, to strona musi być oznaczona jako nie-Cache (inaczej w L1/L2/L3/... pamięci podręcznej będą dane staled). W takich przypadkach procesor zwykle nie będzie zmieniał kolejności odczytu i zapisu (szczegóły sprawdź w instrukcji procesora - może mieć bardziej drobnoziarnistą kontrolę) - kompilator może więc użyć volatile. Jednak ponieważ atomiki są zwykle oparte na pamięci podręcznej, nie potrzebujesz ich lub możesz ich używać.

Obawiam się, że nie mogę odpowiedzieć, jeśli tak silny cache w przyszłych procesorach będzie dostępna spójność. Sugerowałbym ścisłe przestrzeganie specyfikacji ("co jest nie tak w przechowywaniu wskaźnika w int? Z pewnością nikt nie będzie używał więcej niż 4GiB, więc adres 32b jest wystarczająco duży."). Na poprawność odpowiedziali inni, więc nie będę tego uwzględniał.

 4
Author: Maciej Piechotka,
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
2013-03-18 08:47:16

Oto moje zdanie na ten temat, choć nie mam zbyt dużej wiedzy na ten temat, więc weź to z przymrużeniem oka.

Efekt słowa kluczowego volatile może być zależny od kompilatora, ale zakładam, że faktycznie robi to, czego intuicyjnie oczekujemy od niego, a mianowicie uniknie aliasingu lub jakiejkolwiek innej optymalizacji, która nie pozwoli użytkownikowi sprawdzić wartości zmiennej w debuggerze w dowolnym momencie wykonywania tej zmiennej. To dość blisko (i pewnie tak samo) tego odpowiedź na temat znaczenia lotnego.

Bezpośrednia implikacja jest taka, że każdy blok kodu uzyskujący dostęp do zmiennej volatile v będzie musiał przenieść ją do pamięci, gdy tylko ją zmodyfikuje. Ogrodzenia sprawią, że stanie się to w kolejności z innymi aktualizacjami, ale tak czy siak, jeśli v zostanie zmodyfikowane na poziomie źródłowym, będzie tam miejsce na v w wyjściu assembly.

Rzeczywiście, pytanie, które zadajesz jest, jeśli v, załadowany do rejestru, nie został zmodyfikowany przez niektórych obliczenia, co zmusza procesor do ponownego uruchomienia odczytu z v do dowolnego rejestru, w przeciwieństwie do zwykłego ponownego użycia wartości, którą otrzymał wcześniej.

Myślę, że odpowiedź jest taka, że procesorNie Może założyć, że komórka pamięci nie zmieniła się od ostatniego odczytu. Dostęp do pamięci, nawet w systemie jednordzeniowym, nie jest ściśle zarezerwowany dla procesora. Wiele innych podsystemów ma do niego dostęp do odczytu i zapisu(taka jest zasada DMA ).

Najbezpieczniejsza optymalizacja, która Procesor może prawdopodobnie zrobić to sprawdzić, czy wartość została zmieniona w pamięci podręcznej, czy nie, i użyć tego jako podpowiedzi stanu v w pamięci. Pamięci podręczne powinny być zsynchronizowane. z pamięcią dzięki mechanizmom unieważniania pamięci podręcznej dołączonym do DMA. Z tym warunkiem, problem powraca do spójności pamięci podręcznej na wielordzeniowym I "write after write" dla sytuacji wielowątkowych. Ten ostatni problem nie może być skutecznie rozwiązany za pomocą prostych zmiennych volatile, ponieważ ich operacja modyfikacji nie jest atomowa, jak już wiesz.

 1
Author: didierc,
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
2017-05-23 12:33:58