224 votes

Pourquoi devrais-je std::move un std::shared_ptr ?

J'ai parcouru les 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::move un std::shared_ptr ?

Y a-t-il un intérêt à transférer la propriété d'une ressource partagée ?

Pourquoi je ne ferais pas ça à la place ?

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

237voto

David Haim Points 3696

Je pense qu'une chose sur laquelle les autres réponses n'ont pas assez insisté est le point de vitesse .

std::shared_ptr le nombre de références est atomique . augmenter ou diminuer le nombre de références nécessite atomique incrémenter ou décrémenter . C'est cent fois plus lent que non-atomique incrémenter/décrémenter, sans compter que si nous incrémentons et décrémentons le même compteur, nous finissons par obtenir le nombre exact, ce qui entraîne une perte de temps et de ressources considérable.

En déplaçant le shared_ptr au lieu de la copier, nous "volons" la atomique de référence et nous annulons les autres shared_ptr . "voler" le nombre de références n'est pas atomique et c'est cent fois plus rapide que de copier l'adresse de l'utilisateur. shared_ptr (et causant atomique incrémentation ou décrémentation de la référence).

Notez que cette technique est utilisée uniquement à des fins d'optimisation. La copier (comme vous l'avez suggéré) est tout aussi efficace du point de vue fonctionnel.

21 votes

Est-ce que c'est vraiment cent fois plus vite ? Avez-vous des points de repère à ce sujet ?

4 votes

@xaviersjs L'affectation nécessite une incrémentation atomique suivie d'une décrémentation atomique lorsque Value sort de la portée. Les opérations atomiques peuvent prendre des centaines de cycles d'horloge. Donc oui, c'est vraiment beaucoup plus lent.

4 votes

@Adisak c'est la première fois que j'entends parler de l'opération de récupération et d'ajout ( fr.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 à ce sujet ?

146voto

Bo Persson Points 42821

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

2 votes

N'est-ce pas une optimisation prématurée ?

14 votes

@YSC pas si celui qui l'a mis là l'a testé.

25 votes

@YSC L'optimisation prématurée est un mal si elle rend le code plus difficile à lire ou à maintenir. Celle-ci ne fait ni l'un ni l'autre, du moins selon l'OMI.

83voto

Mr.C64 Points 11681

Déplacements (comme le constructeur de déplacement) pour std::shared_ptr sont bon marché car ils sont fondamentalement "vol de conseils" (de la source à la destination ; pour être plus précis, l'ensemble du 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, copie les opérations sur std::shared_ptr invoquez atomique l'augmentation du nombre de références (c'est-à-dire qu'il ne s'agit pas uniquement de ++RefCount sur un nombre entier RefCount mais, par exemple, en appelant InterlockedIncrement sous Windows), qui est plus coûteux que de simplement voler des pointeurs/états.

Il faut donc analyser en détail la dynamique du comptage des références dans ce cas :

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

Si vous passez sp par valeur et ensuite prendre un copie à l'intérieur de la CompilerInstance::setInvocation méthode, vous avez :

  1. Lors de l'entrée dans la méthode, le shared_ptr le paramètre est construit par copie : ref count atomique incrémenter .
  2. Dans le corps de la méthode, vous copie le site shared_ptr dans l'élément de données : ref count atomique incrémenter .
  3. À la sortie de la méthode, l shared_ptr le paramètre est détruit : ref count atomique décrémenter .

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

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

  1. Lors de l'entrée dans la méthode, le shared_ptr le paramètre est construit par copie : ref count atomique incrémenter .
  2. Dans le corps de la méthode, vous std::move le site shared_ptr dans l'élément de données : ref count ne pas changement ! Vous ne faites que voler des pointeurs/états : aucune opération atomique coûteuse de comptage de ref n'est impliquée.
  3. À la sortie de la méthode, l shared_ptr est détruit ; mais comme vous vous êtes déplacé à l'étape 2, il n'y a rien à détruire, puisque le paramètre shared_ptr ne pointe plus sur rien. Encore une fois, aucune décrémentation atomique ne se produit dans ce cas.

En résumé, dans ce cas, vous obtenez seulement un ref count atomic increment, c'est à dire juste un atome fonctionnement.
Comme vous pouvez le voir, c'est beaucoup meilleur que deux incréments atomiques plus un décrément atomique (pour un total de trois opérations atomiques) pour le cas de la copie.

1 votes

A noter également : pourquoi ne passent-ils pas simplement par référence constante, et évitent-ils tout le truc std::move ? Parce que pass-by-value vous permet également de passer un pointeur brut directement et il y aura juste un shared_ptr créé.

0 votes

@JosephIreland Parce que vous ne pouvez pas déplacer une référence const.

2 votes

@JosephIreland parce que si vous l'appelez comme compilerInstance.setInvocation(std::move(sp)); alors il n'y aura pas incrémenter . Vous pouvez obtenir le même comportement en ajoutant une surcharge qui prend un fichier shared_ptr<>&& mais pourquoi dupliquer quand vous n'en avez pas besoin.

27voto

Stephen C. Steel Points 2869

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

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

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 prévue est un transfert jusqu'à ce que vous vérifiiez que le propriétaire original renonce immédiatement à la propriété. En prime, une implémentation plus efficace est possible, puisqu'un transfert atomique de propriété peut éviter l'état temporaire où le nombre de propriétaires a augmenté de un (et les changements qui en découlent dans le nombre de références).

1 votes

Exactement ce que je cherche. Je suis surpris de voir que les autres réponses ignorent cette différence sémantique importante. Les pointeurs intelligents sont une question de propriété.

1 votes

Je pense que la propriété est particulièrement cruciale dans la notation lambda. La capture d'un ptr partagé par référence peut ne pas contribuer à son compteur de référence et après la sortie du code et la destruction du ptr, vous auriez une lambda avec un pointeur suspendu.

26voto

SingerOfTheFall Points 9936

Copie d'un shared_ptr implique de copier son pointeur d'objet d'état interne et de modifier le nombre de références. Le déplacer n'implique que l'échange des pointeurs du compteur de référence interne et de l'objet possédé, c'est donc plus rapide.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X