Sto scrivendo un codice senza blocco, e ho trovato un modello interessante, ma non sono sicuro che si comporti come previsto in ordine di memoria rilassato.Quali sono le garanzie di ordinazione della memoria C++ 11 in questo caso angolare?
Il modo più semplice per spiegarlo è con un esempio:
std::atomic<int> a, b, c;
auto a_local = a.load(std::memory_order_relaxed);
auto b_local = b.load(std::memory_order_relaxed);
if (a_local < b_local) {
auto c_local = c.fetch_add(1, std::memory_order_relaxed);
}
Nota che tutte le operazioni utilizzano std::memory_order_relaxed
.
Ovviamente, sul thread su cui viene eseguito questo, i carichi per a
e b
devono essere eseguiti prima che venga valutata la condizione if
.
Analogamente, l'operazione di lettura-modifica-scrittura (RMW) su c
deve essere eseguita dopo che la condizione è stata valutata (perché è condizionata a quella condizione ...).
Quello che voglio sapere è, questo codice garantisce che il valore di c_local
è aggiornato almeno quanto i valori di a_local
e b_local
? Se è così, com'è possibile dato l'ordine di memoria rilassato? La dipendenza del controllo insieme all'operazione RWM agisce come una specie di recinto di acquisizione? (Nota che non c'è nemmeno una versione corrispondente da nessuna parte.)
Se quanto sopra è vero, credo che questo esempio dovrebbe funzionare anche (supponendo che non ci sia un overflow) - ho ragione?
std::atomic<int> a(0), b(0);
// Thread 1
while (true) {
auto a_local = a.fetch_add(1, std::memory_order_relaxed);
if (a_local >= 0) { // Always true at runtime
b.fetch_add(1, std::memory_order_relaxed);
}
}
// Thread 2
auto b_local = b.load(std::memory_order_relaxed);
if (b_local < 777) {
// Note that fetch_add returns the pre-incrementation value
auto a_local = a.fetch_add(1, std::memory_order_relaxed);
assert(b_local <= a_local); // Is this guaranteed?
}
Il filo 1, v'è una dipendenza di controllo che sospetto garantisce che a
viene sempre incrementato prima b
viene incrementato (ma ogni conservazione essendo incrementato collo-e-collo). Sul thread 2 esiste un'altra dipendenza di controllo che, a mio avviso, garantisce che b
venga caricato in b_local
prima che venga incrementato a
. Ritengo inoltre che il valore restituito da fetch_add
sarà almeno il più recente di qualsiasi valore osservato in b_local
e che pertanto è necessario mantenere assert
. Ma non sono sicuro, dal momento che questo si discosta significativamente dai soliti esempi di ordinamento della memoria, e la mia comprensione del modello di memoria C++ 11 non è perfetta (ho problemi a ragionare su questi effetti di ordinamento della memoria con qualsiasi grado di certezza). Ogni approfondimento è apprezzato!
Aggiornamento: Come bames53 ha utilmente sottolineato nei commenti, dato un sufficientemente intelligente compilatore, è possibile che un if
potrebbe essere ottimizzato interamente nelle giuste circostanze, nel qual caso i carichi rilassato potrebbero essere riordinato per verificarsi dopo il RMW, causando i loro valori per essere più aggiornati rispetto al valore di ritorno fetch_add
(il assert
potrebbe sparare nel mio secondo esempio). Tuttavia, cosa succede se invece di un if
, è inserito uno atomic_signal_fence
(non atomic_thread_fence
)? Questo certamente non può essere ignorato dal compilatore, non importa quali siano le ottimizzazioni, ma garantisce che il codice si comporti come previsto? In questo caso, la CPU è autorizzata a eseguire un nuovo ordine?
Il secondo esempio diventa allora:
std::atomic<int> a(0), b(0);
// Thread 1
while (true) {
auto a_local = a.fetch_add(1, std::memory_order_relaxed);
std::atomic_signal_fence(std::memory_order_acq_rel);
b.fetch_add(1, std::memory_order_relaxed);
}
// Thread 2
auto b_local = b.load(std::memory_order_relaxed);
std::atomic_signal_fence(std::memory_order_acq_rel);
// Note that fetch_add returns the pre-incrementation value
auto a_local = a.fetch_add(1, std::memory_order_relaxed);
assert(b_local <= a_local); // Is this guaranteed?
Un altro aggiornamento: Dopo aver letto tutte le risposte finora e pettinatura attraverso lo standard me stesso, non credo che si possa dimostrare che la il codice è corretto usando solo lo standard. Quindi, qualcuno può inventarsi un contro-esempio di un sistema teorico conforme allo standard e anche lanciare l'asserzione?
Penso che debba essere la natura dell'operazione 'fetch_add', e non la dipendenza di controllo che fa sì che l'asserzione sia vera. Non riesco a trovare nulla che indicherebbe che una dipendenza di controllo causerebbe una sincronizzazione aggiuntiva oltre a quella di una relazione sequenziata prima. –
@Vaughn: Questo ha senso, anche se sembra ancora non intuitivo per me. Senza la dipendenza del controllo, però, l'ordine rilassato potrebbe causare il verificarsi dei carichi dopo 'fetch_add' - quindi si stanno sincronizzando, non riesco proprio a capire in quale capacità. Forse tutte le relazioni sequenziate prima avrebbero lo stesso effetto qui? – Cameron
È una situazione davvero interessante. Voglio esaminare attentamente le regole e vedere quali sono le possibilità. Sto pensando che il 'if' sta limitando il tipo di riordino che il compilatore può fare nella pratica, anche se forse non in teoria. Inoltre, sembrerebbe che 'fetch_add' debba limitare i valori che possono essere visti dall'ordine di modifica. Penso che quando si 'fetch_add', il valore che si ottiene non possa essere prima di qualsiasi modifica avvenuta prima di' fetch_add'. Quale non sarebbe il caso con un 'carico' regolare. –