Pourquoi devrais-je std :: déplacer un std :: shared_ptr?

148

J'ai parcouru le code source de Clang et j'ai trouvé cet extrait:

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

Pourquoi voudrais-je std::moveun std::shared_ptr?

Y a-t-il un point de transfert de propriété sur une ressource partagée?

Pourquoi ne ferais-je pas ça à la place?

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

Réponses:

137

Je pense que la seule chose que les autres réponses n'ont pas suffisamment soulignée est le point de vitesse .

std::shared_ptrle nombre de références est atomique . l'augmentation ou la diminution du nombre de références nécessite un incrément ou un décrément atomique . C'est cent fois plus lent que l' incrémentation / décrémentation non atomique , sans compter que si nous incrémentons et décrémentons le même compteur, nous nous retrouvons avec le nombre exact, gaspillant une tonne de temps et de ressources dans le processus.

En déplaçant le shared_ptrau lieu de le copier, nous "volons" le nombre de références atomiques et nous annulons l'autre shared_ptr. "voler" le compte de référence n'est pas atomique , et c'est cent fois plus rapide que de copier le shared_ptr(et de provoquer un incrément ou une diminution de référence atomique ).

Notez que cette technique est utilisée uniquement à des fins d'optimisation. le copier (comme vous l'avez suggéré) est tout aussi fin en termes de fonctionnalité.

David Haim
la source
5
Est-ce vraiment cent fois plus rapide? Avez-vous des repères pour cela?
xaviersjs
1
@xaviersjs L'affectation nécessite un incrément atomique suivi d'un décrément atomique lorsque Value sort de la portée. Les opérations atomiques peuvent prendre des centaines de cycles d'horloge. Alors oui, c'est vraiment beaucoup plus lent.
Adisak
2
@Adisak c'est la première fois que j'ai entendu l'opération d'extraction et d'ajout ( en.wikipedia.org/wiki/Fetch-and-add ) pourrait prendre des centaines de cycles de plus qu'un incrément de base. Avez-vous une référence pour cela?
xaviersjs
2
@xaviersjs: stackoverflow.com/a/16132551/4238087 Les opérations de registre étant de quelques cycles, des centaines (100-300) de cycles pour atomique correspondent à la facture. Bien que les métriques datent de 2013, cela semble toujours être vrai, en particulier pour les systèmes NUMA multi-sockets.
russianfool
1
Parfois, vous pensez qu'il n'y a pas de threading dans votre code ... mais alors une sacrée bibliothèque arrive et la ruine pour vous. Mieux vaut utiliser des références const et std :: move ... s'il est clair et évident que vous pouvez ... que de compter sur le nombre de références de pointeurs.
Erik Aronesty le
123

En utilisant, movevous évitez d'augmenter, puis de diminuer immédiatement, le nombre d'actions. Cela pourrait vous faire économiser des opérations atomiques coûteuses sur le nombre d'utilisation.

Bo Persson
la source
1
N'est-ce pas une optimisation prématurée?
YSC
11
@YSC pas si celui qui l'a mis là-bas l'a testé.
OrangeDog
19
@YSC L'optimisation prématurée est mauvaise si elle rend le code plus difficile à lire ou à maintenir. Celui-ci ne fait ni l'un ni l'autre, du moins l'OMI.
Angew n'est plus fier de SO
17
En effet. Ce n'est pas une optimisation prématurée. C'est plutôt la manière judicieuse d'écrire cette fonction.
Courses de légèreté en orbite le
60

Les opérations de déplacement (comme le constructeur de déplacement) pour std::shared_ptrsont bon marché , car ce sont essentiellement des "pointeurs de vol" (de la source à la destination; pour être plus précis, tout le bloc de contrôle d'état est "volé" de la source à la destination, y compris les informations de comptage de référence) .

Au lieu de cela, copiez les opérations sur std::shared_ptrinvoquer l' augmentation du nombre de références atomiques (c'est-à-dire pas seulement ++RefCountsur un RefCountmembre de données entier , mais par exemple en appelant InterlockedIncrementsur Windows), ce qui est plus coûteux que de simplement voler des pointeurs / état.

Donc, en analysant la dynamique du nombre de ref de ce cas en détail:

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

Si vous passez sppar valeur puis en prenez une copie à l'intérieur de la CompilerInstance::setInvocationméthode, vous avez:

  1. Lors de la saisie de la méthode, le shared_ptrparamètre est construit par copie: ref count incrément atomique .
  2. Dans le corps de la méthode, vous copiez le shared_ptrparamètre dans le membre de données: ref count atomic increment .
  3. A la sortie de la méthode, le shared_ptrparamètre est détruit: ref count décrément atomique .

Vous avez deux incréments atomiques et un décrément atomique, pour un total de trois opérations atomiques .

Au lieu de cela, si vous passez le shared_ptrparamètre par valeur, puis std::moveà l'intérieur de la méthode (comme correctement fait dans le code de Clang), vous avez:

  1. Lors de la saisie de la méthode, le shared_ptrparamètre est construit par copie: ref count incrément atomique .
  2. Dans le corps de la méthode, vous placez std::movele shared_ptrparamètre dans le membre de données: le nombre de références ne change pas ! Vous ne faites que voler des pointeurs / état: aucune opération coûteuse de comptage de références atomiques n'est impliquée.
  3. Lorsque vous quittez la méthode, le shared_ptrparamètre est détruit; mais comme vous vous êtes déplacé à l'étape 2, il n'y a rien à détruire, car le shared_ptrparamètre ne pointe plus vers rien. Encore une fois, aucun décrément atomique ne se produit dans ce cas.

Bottom line: dans ce cas, vous obtenez un seul incrément atomique de ref count, c'est-à-dire une seule opération atomique .
Comme vous pouvez le voir, c'est bien mieux que deux incréments atomiques plus un décrément atomique (pour un total de trois opérations atomiques) pour le cas de copie.

Monsieur C64
la source
1
Il convient également de noter: pourquoi ne passent-ils pas simplement par référence à const et évitent tout ce qui concerne std :: move? Parce que le passage par valeur vous permet également de passer directement un pointeur brut et il n'y aura qu'un seul shared_ptr créé.
Joseph Ireland
@JosephIreland Parce que vous ne pouvez pas déplacer une référence const
Bruno Ferreira
2
@JosephIreland car si vous l'appelez ainsi, il compilerInstance.setInvocation(std::move(sp));n'y aura pas d' incrément . Vous pouvez obtenir le même comportement en ajoutant une surcharge qui prend un shared_ptr<>&&mais pourquoi se dupliquer lorsque vous n'en avez pas besoin.
ratchet freak
2
@BrunoFerreira Je répondais à ma propre question. Vous n'avez pas besoin de le déplacer car c'est une référence, copiez-la simplement. Encore un seul exemplaire au lieu de deux. La raison pour laquelle ils ne le font pas est que cela copierait inutilement des shared_ptrs nouvellement construits, par exemple depuis setInvocation(new CompilerInvocation)ou comme ratchet mentionné setInvocation(std::move(sp)). Désolé si mon premier commentaire n'était pas clair, je l'ai en fait posté par accident, avant d'avoir fini d'écrire, et j'ai décidé de le laisser
Joseph Ireland
22

La copie d'un shared_ptrimplique la copie de son pointeur d'objet d'état interne et la modification du nombre de références. Le déplacer implique uniquement d'échanger des pointeurs vers le compteur de référence interne et l'objet possédé, donc c'est plus rapide.

SingerOfTheFall
la source
16

Il y a deux raisons d'utiliser std :: move dans cette situation. La plupart des réponses abordaient la question de la vitesse, mais ignoraient l'importante question de montrer plus clairement l'intention du code.

Pour un std :: shared_ptr, std :: move dénote sans ambiguïté un transfert de propriété du pointé, tandis qu'une simple opération de copie ajoute un propriétaire supplémentaire. Bien sûr, si le propriétaire d'origine renonce par la suite à sa propriété (par exemple en autorisant la destruction de son std :: shared_ptr), alors un transfert de propriété a été effectué.

Lorsque vous transférez la propriété avec std :: move, ce qui se passe est évident. Si vous utilisez une copie normale, il n'est pas évident que l'opération envisagée soit un transfert tant que vous n'avez pas vérifié que le propriétaire d'origine renonce immédiatement à la propriété. En prime, une mise en œuvre plus efficace est possible, car un transfert atomique de propriété peut éviter l'état temporaire où le nombre de propriétaires a augmenté de un (et les changements de référence qui en découlent).

Stephen C. Steel
la source
Exactement ce que je recherche. Surpris comment les autres réponses ignorent cette différence sémantique importante. les pointeurs intelligents sont une question de propriété.
qweruiop