Perché dovrei std :: move an std :: shared_ptr?


148

Ho cercato il codice sorgente di Clang e ho trovato questo frammento:

void CompilerInstance::setInvocation(
    std::shared_ptr<CompilerInvocation> Value) {
  Invocation = std::move(Value);
}

Perché dovrei voler std::moveun std::shared_ptr?

C'è qualche punto che trasferisce la proprietà su una risorsa condivisa?

Perché non dovrei fare questo invece?

void CompilerInstance::setInvocation(
    std::shared_ptr<CompilerInvocation> Value) {
  Invocation = Value;
}

Risposte:


137

Penso che l'unica cosa che l'altra risposta non abbia sottolineato abbastanza sia il punto di velocità .

std::shared_ptril conteggio dei riferimenti è atomico . l'aumento o la riduzione del conteggio di riferimento richiede un incremento o un decremento atomico . Questo è cento volte più lento di quanto non-atomica incremento / decremento, per non parlare che se noi di incremento e decremento lo stesso contatore finiamo con il numero esatto, sprecando un sacco di tempo e risorse nel processo.

Spostando shared_ptrinvece di copiarlo, "rubiamo" il conteggio dei riferimenti atomici e annulliamo l'altro shared_ptr. "rubare" il conteggio dei riferimenti non è atomico , ed è cento volte più veloce rispetto alla copia del shared_ptr(e causando incremento o decremento del riferimento atomico ).

Si noti che questa tecnica viene utilizzata esclusivamente per l'ottimizzazione. copiarlo (come hai suggerito) è altrettanto funzionale dal punto di vista funzionale.


5
È davvero cento volte più veloce? Hai dei benchmark per questo?
xaviersjs,

1
@xaviersjs L'assegnazione richiede un incremento atomico seguito da un decremento atomico quando Valore esce dall'ambito. Le operazioni atomiche possono richiedere centinaia di cicli di clock. Quindi sì, è davvero molto più lento.
Adisak,

2
@Adisak è il primo che ho sentito l'operazione di recupero e aggiunta ( en.wikipedia.org/wiki/Fetch-and-add ) potrebbe richiedere centinaia di cicli più di un incremento di base. Hai un riferimento per questo?
xaviersjs,

2
@xaviersjs: stackoverflow.com/a/16132551/4238087 Con le operazioni di registro di essere un paio di cicli, 100 di (100-300) di cicli per attacchi atomici il conto. Sebbene le metriche siano del 2013, questo sembra essere vero soprattutto per i sistemi NUMA multi-socket.
russianfool

1
A volte pensi che non ci sia thread nel tuo codice ... ma poi arriva qualche libreria maledetta che rovina tutto per te. Meglio usare i riferimenti const e std :: move ... se è chiaro e ovvio che puoi .... che fare affidamento sui conteggi dei riferimenti del puntatore.
Erik Aronesty,

123

Usando movesi evita di aumentare, e quindi immediatamente diminuire, il numero di condivisioni. Ciò potrebbe farti risparmiare alcune costose operazioni atomiche sul conteggio degli usi.


1
Non è un'ottimizzazione prematura?
YSC,

11
@YSC no se chiunque lo ha messo lì lo ha effettivamente testato.
OrangeDog,

19
@YSC L'ottimizzazione precoce è un male se rende il codice più difficile da leggere o mantenere. Questo non fa nessuno dei due, almeno IMO.
Angew non è più orgoglioso di SO

17
Infatti. Questa non è un'ottimizzazione prematura. È invece il modo ragionevole di scrivere questa funzione.
Corse di leggerezza in orbita il

60

Le operazioni di spostamento (come il costruttore di spostamenti) std::shared_ptrsono economiche , in quanto sostanzialmente "rubano puntatori" (dalla sorgente alla destinazione; per essere più precisi, l'intero blocco di controllo dello stato viene "rubato" dalla sorgente alla destinazione, comprese le informazioni sul conteggio dei riferimenti) .

Invece le operazioni di copiastd::shared_ptr sull'invocazione aumentano il conteggio dei riferimenti atomici (cioè non solo ++RefCountsu un RefCountmembro di dati interi , ma ad esempio chiamando InterlockedIncrementsu Windows), che è più costoso del semplice rubare puntatori / stato.

Quindi, analizzando in dettaglio la dinamica del conteggio di riferimento di questo caso:

// shared_ptr<CompilerInvocation> sp;
compilerInstance.setInvocation(sp);

Se passi spper valore e poi ne esegui una copia all'interno del CompilerInstance::setInvocationmetodo, hai:

  1. Quando si immette il metodo, il shared_ptrparametro viene creato come copia: incremento del conteggio atomico .
  2. All'interno del corpo del metodo, si copia il shared_ptrparametro nel membro dati: ref count incremento atomico .
  3. All'uscita dal metodo, il shared_ptrparametro viene distrutto: ref count decremento atomico .

Hai due incrementi atomici e un decremento atomico, per un totale di tre operazioni atomiche .

Invece, se passi il shared_ptrparametro per valore e poi std::moveall'interno del metodo (come correttamente fatto nel codice di Clang), hai:

  1. Quando si immette il metodo, il shared_ptrparametro viene creato come copia: incremento del conteggio atomico .
  2. All'interno del corpo del metodo, è std::moveil shared_ptrparametro nell'elemento dati: il conteggio dei riferimenti non cambia! Stai solo rubando puntatori / stato: non sono coinvolte costose operazioni di conteggio dei riferimenti atomici.
  3. All'uscita dal metodo, il shared_ptrparametro viene distrutto; ma poiché ti sei spostato nel passaggio 2, non c'è nulla da distruggere, poiché il shared_ptrparametro non punta più a nulla. Ancora una volta, in questo caso non si verifica alcun decremento atomico.

In conclusione: in questo caso si ottiene solo un incremento atomico del conteggio dei riferimenti, ovvero solo un'operazione atomica .
Come puoi vedere, è molto meglio di due incrementi atomici più un decremento atomico (per un totale di tre operazioni atomiche) per il caso della copia.


1
Vale anche la pena notare: perché non passano semplicemente per riferimento const ed evitano l'intera roba std :: move? Perché il valore pass-by ti consente anche di passare direttamente un puntatore non elaborato e ne verrà creato solo uno_ptr.
Joseph Ireland,

@JosephIreland Perché non puoi spostare un riferimento const
Bruno Ferreira,

2
@JosephIreland perché se lo chiami come compilerInstance.setInvocation(std::move(sp));allora non ci saranno incrementi . È possibile ottenere lo stesso comportamento aggiungendo un sovraccarico che richiede un shared_ptr<>&&ma perché duplicare quando non è necessario.
Cricchetto maniaco

2
@BrunoFerreira Stavo rispondendo alla mia domanda. Non è necessario spostarlo perché è un riferimento, basta copiarlo. Ancora solo una copia anziché due. Il motivo per cui non lo fanno è perché inutilmente copiare shared_ptrs nuova costruzione, ad esempio da setInvocation(new CompilerInvocation), o come cricchetto menzionato, setInvocation(std::move(sp)). Scusate se il mio primo commento non è chiaro, l'ho pubblicato per caso, prima di aver finito di scrivere, e ho deciso di lasciarlo
Joseph Irlanda,

22

La copia di a shared_ptrcomporta la copia del puntatore dell'oggetto stato interno e la modifica del conteggio dei riferimenti. Lo spostamento implica solo lo scambio di puntatori al contatore di riferimento interno e all'oggetto posseduto, quindi è più veloce.


16

Ci sono due ragioni per usare std :: move in questa situazione. La maggior parte delle risposte ha affrontato il problema della velocità, ma ha ignorato l'importante problema di mostrare più chiaramente l'intento del codice.

Per uno std :: shared_ptr, std :: move indica inequivocabilmente un trasferimento di proprietà della punta, mentre una semplice operazione di copia aggiunge un ulteriore proprietario. Naturalmente, se il proprietario originale successivamente cede la propria proprietà (ad esempio consentendo la distruzione del proprio std :: shared_ptr), è stato eseguito un trasferimento di proprietà.

Quando trasferisci la proprietà con std :: move, è ovvio cosa sta succedendo. Se si utilizza una copia normale, non è ovvio che l'operazione prevista è un trasferimento fino a quando non si verifica che il proprietario originale ceda immediatamente la proprietà. Come bonus, è possibile un'implementazione più efficiente, dal momento che un trasferimento atomico di proprietà può evitare lo stato temporaneo in cui il numero di proprietari è aumentato di uno (e l'operatore cambia nei conteggi di riferimento).


Esattamente quello che sto cercando. Sorpreso di come le altre risposte ignorino questa importante differenza semantica. i puntatori intelligenti riguardano la proprietà.
Qweruiop,

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.