[[carry_dependency]] cosa significa e come implementarlo
Stavo leggendo di [[carry_dependency]] in questo post SO .
Ma quello che non sono riuscito a capire sono le seguenti frasi nella risposta accettata:
"In particolare, se un valore letto con memory_order_consume viene passato a una funzione, quindi senza [[carry_dependency]], il compilatore potrebbe dover emettere un'istruzione di recinzione della memoria per garantire che la semantica di ordinamento della memoria appropriata venga mantenuta. Se il parametro è annotato con [[carry_dependency]], quindi il compilatore può presumere che il corpo della funzione trasporterà correttamente la dipendenza e questa barriera potrebbe non essere più necessaria.
Allo stesso modo, se una funzione restituisce un valore caricato con memory_order_consume, o derivato da tale valore, allora senza [[carry_dependency]] potrebbe essere richiesto al compilatore di inserire un'istruzione di fence per garantire che la semantica di ordinamento della memoria appropriata venga mantenuta. Con l'annotazione [[carry_dependency]], questa barriera potrebbe non essere più necessaria, poiché il chiamante è ora responsabile del mantenimento dell'albero delle dipendenze. "
Facciamo un passo dopo passo:
"se un valore letto con memory_order_consume viene passato a una funzione, quindi senza [[carry_dependency]], il compilatore potrebbe dover emettere un'istruzione di recinzione della memoria per garantire che la semantica di ordinamento della memoria appropriata venga mantenuta."
Quindi, per una variabile atomica nel modello di memoria a consumo di rilascio quando la variabile atomica viene passata come parametro alla funzione, il compilatore introdurrà un'istruzione hardware di recinzione in modo che abbia sempre il valore più recente e aggiornato della variabile atomica fornita alla funzione.
Il prossimo -
"Se il parametro è annotato con [[carry_dependency]], il compilatore può presumere che il corpo della funzione trasporterà correttamente la dipendenza e questa barriera potrebbe non essere più necessaria."
Questo mi confonde: il valore della variabile atomica è già consumato e quindi quale dipendenza viene eseguita la funzione?
Allo stesso modo -
"se una funzione restituisce un valore caricato con memory_order_consume, o derivato da tale valore, allora senza [[carry_dependency]] potrebbe essere richiesto al compilatore di inserire un'istruzione di fence per garantire che la semantica di ordinamento della memoria appropriata venga mantenuta. Con [[ porta_dipendenza]], questa barriera potrebbe non essere più necessaria, poiché il chiamante è ora responsabile del mantenimento dell'albero delle dipendenze. "
Dall'esempio non è chiaro quale sia il punto che sta cercando di affermare riguardo al portare la dipendenza?
Risposte
Solo per tua informazione, memory_order_consume(e [[carries_dependency]]) è essenzialmente deprecato perché è troppo difficile per i compilatori implementare in modo efficiente e corretto le regole nel modo in cui le ha progettate C ++ 11. (E / o perché [[carries_dependency]]e / o kill_dependencyfinirebbero per essere necessari ovunque .) Vedere P0371R1: Scoraggia temporaneamente memory_order_consume .
I compilatori attuali trattano semplicemente mo_consumecome mo_acquire(e quindi sugli ISA che ne hanno bisogno, mettono una barriera subito dopo il carico di consumo). Se si desidera che le prestazioni dell'ordinamento in base alla dipendenza dei dati siano senza barriere, è necessario ingannare il compilatore utilizzando mo_relaxede codice con attenzione per evitare cose che renderebbero probabile che il compilatore crei asm senza una reale dipendenza. (ad esempio Linux RCU). Vedi C ++ 11: la differenza tra memory_order_relaxed e memory_order_consume per maggiori dettagli e collegamenti a riguardo, e la funzionalità asm che è mo_consumestata progettata per esporre.
Anche l' ordine di memoria consuma l'utilizzo in C11 .
Comprendere il concetto di ordinamento delle dipendenze (in asm) è fondamentalmente essenziale per capire come è progettata questa funzionalità C ++.
Quando [una] variabile atomica viene passata come parametro alla funzione, il compilatore introdurrà un'istruzione hardware di fence ...
In primo luogo non si "passa una variabile atomica" a una funzione; cosa significherebbe? Se si passasse un puntatore o un riferimento a un oggetto atomico, la funzione eseguirà il proprio caricamento da esso e il codice sorgente per quella funzione lo userebbe memory_order_consumeo meno.
La cosa rilevante è passare un valore caricato da una variabile atomica con mo_consume. Come questo:
int tmp = shared_var.load(std::memory_order_consume);
func(tmp);
funcpuò usare quell'argomento come indice in un array di atomic<int>per eseguire un mo_relaxedcaricamento. Affinché quel carico venga ordinato in base alle dipendenze shared_var.loadanche senza una barriera di memoria, code-gen for funcdeve assicurarsi che il carico abbia una dipendenza dei dati asm dall'arg, anche se il codice C ++ fa qualcosa del genere tmp -= tmp;che i compilatori normalmente tratterebbero solo il uguale a tmp = 0;(uccidere il valore precedente).
Ma [[carries_dependency]]il compilatore farebbe ancora riferimento a quel valore azzerato con una dipendenza dai dati nell'implementazione di qualcosa di simile array[idx+tmp].
il valore della variabile atomica è già consumato e quindi quale dipendenza è svolta la funzione?
"Già consumato" non è un concetto valido. Il punto centrale consumeinvece di acquireè che i carichi successivi vengono ordinati correttamente perché hanno una dipendenza dei dati dal mo_consumerisultato del caricamento, consentendo di evitare barriere. Ogni caricamento successivo necessita di tale dipendenza se lo si desidera ordinato dopo il caricamento originale; non ha senso dire che un valore è "già consumato".
Se finisci per inserire una barriera per promuovere il consumo da acquisire a causa della mancanza di carry_dependency su una funzione, le funzioni successive non avrebbero bisogno di un'altra barriera perché potresti dire che il valore era "già acquisito". (Anche se non è una terminologia standard. Dovresti invece dire codice dopo che la prima barriera è stata ordinata dopo il caricamento.)
Potrebbe essere utile capire come il kernel Linux gestisce questo, con le loro atomiche fatte a mano e il set limitato di compilatori che supportano. Cerca "dipendenza" inhttps://github.com/torvalds/linux/blob/master/Documentation/memory-barriers.txte nota la differenza tra una "dipendenza del controllo" come if(flag) data.load()una dipendenza dai dati come data[idx].load.
IIRC, anche C ++ non garantisce l' mo_consumeordinamento delle dipendenze quando la dipendenza è un simile if(x.load(consume)) tmp=y.load();.
Si noti che i compilatori a volte trasformano una dipendenza dai dati in una dipendenza dal controllo se, ad esempio, sono presenti solo 2 valori possibili. Ciò non funzionerebbe mo_consumee sarebbe un'ottimizzazione che non sarebbe consentita se il valore provenisse da un mo_consumecarico o da una [[carries_dependency]]funzione arg. Questo fa parte del motivo per cui è difficile da implementare; richiederebbe l'insegnamento di molti passaggi di ottimizzazione sull'ordinamento della dipendenza dai dati invece di aspettarsi che gli utenti scrivano codice che non fa cose che normalmente ottimizzeranno. (Mi piace tmp -= tmp;)