Czy zmienne, ale nieudostępnione odczyty dają nieskończenie stare wartości? (na prawdziwym sprzęcie)
W odpowiedzito pytanie Pojawiło się kolejne pytanie dotyczące sytuacji OP, o których nie byłem pewien: to głównie pytanie o architekturę procesora, ale z pytaniem domowym dotyczącym modelu pamięci C ++ 11.
Zasadniczo kod OP był zapętlony w nieskończoność przy wyższych poziomach optymalizacji z powodu następującego kodu (nieco zmodyfikowanego 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()
Czy wbudowany jest atomowy CAS GCC. GCC (rozsądnie) zoptymalizowało to w nieskończoną pętlębits_ & mask
został wykrytytrue
przed wejściem do pętli, pomijając całkowicie operację CAS, więc zaproponował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 zmianabits_
dovolatile uint8_t
wydaje się również działać. Zaproponowałem, aby nie iść tą drogą, ponieważvolatile
normalnie nie powinno być używane do synchronizacji, a poza tym nie wydaje się, aby korzystanie z ogrodzenia było w tym przypadku dużo gorsze.
Jednak myślałem o tym więcej i w tym przypadku semantyka jest taka, że nie ma znaczenia, czyov & MASK
sprawdzanie opiera się na nieaktualnej wartości, o ile nie opiera się na nieskończenie nieaktualnej (tj. tak długo, jak długo pętla jest w końcu zerwana), ponieważ rzeczywista próba aktualizacjibits_
jest zsynchronizowany. Więc jestvolatile
wystarczy, aby zagwarantować, że ta pętla zakończy się ostatecznie, jeślibits_
jest aktualizowany przez inny wątek taki, żebits_ & MASK == false
, dla każdego istniejącego procesora? Innymi słowy, przy braku wyraźnego ogrodzenia pamięci, czy jest praktycznie możliwe, aby odczyty nie zoptymalizowane przez kompilator były skutecznie zoptymalizowane przez procesor w nieskończoność? (EDYTOWAĆ: Żeby było jasne, pytam tutaj o to, co nowoczesny sprzęt może faktycznie zrobić, biorąc pod uwagę założenie, że kompilator wysyła emisje w pętli, więc technicznie nie jest to pytanie językowe, chociaż wyrażanie go w kategoriach semantyki C ++ jest wygodne.)
Jest to kąt sprzętowy, ale aby go nieco zaktualizować, a także zadać pytanie dotyczące modelu pamięci C ++ 11, należy wziąć pod uwagę następującą odmianę powyższego kodu:
// 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 żestd::memory_order_relaxed
sugeruje, że „nie ma ograniczeń co do 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 to, żebits_.load(std::memory_order_relaxed)
technicznienigdy odczytaj zaktualizowaną wartość pobits_
jest aktualizowany w innym wątku zgodnej implementacji?
EDYTOWAĆ: Znalazłem to w standardzie (29,4 p13):
Implementacje powinny sprawić, że zapasy atomowe będą widoczne dla ładunków atomowych w rozsądnym czasie.
Tak więc najwyraźniej czekanie „nieskończenie długo” na zaktualizowaną wartość jest (głównie?) Poza pytaniem, ale nie ma żadnej twardej gwarancji, że określony przedział czasu świeżości jest inny niż powinien być „rozsądny”; nadal pozostaje pytanie o rzeczywiste zachowanie sprzętu.