Existe-t-il un équivalent non atomique de std :: shared_ptr? Et pourquoi n'y en a-t-il pas un dans <memory>?

88

C'est un peu une question en deux parties, tout sur l'atomicité de std::shared_ptr:

1. Autant que je sache, std::shared_ptrc'est le seul pointeur intelligent <memory>qui soit atomique. Je me demande s'il existe une version non atomique de std::shared_ptrdisponible (je ne vois rien dedans <memory>, donc je suis également ouvert aux suggestions en dehors du standard, comme celles de Boost). Je sais que boost::shared_ptrc'est aussi atomique (si ce BOOST_SP_DISABLE_THREADSn'est pas défini), mais peut-être qu'il y a une autre alternative? Je recherche quelque chose qui a la même sémantique que std::shared_ptr, mais sans l'atomicité.

2. Je comprends pourquoi std::shared_ptrest atomique; c'est plutôt sympa. Cependant, ce n'est pas bien pour toutes les situations, et C ++ a toujours eu pour mantra "ne payez que pour ce que vous utilisez". Si je n'utilise pas plusieurs threads, ou si j'utilise plusieurs threads mais que je ne partage pas la propriété du pointeur entre les threads, un pointeur intelligent atomique est excessif. Ma deuxième question est pourquoi une version non atomique de std::shared_ptrC ++ 11 n'a pas été fournie ? (en supposant qu'il y ait un pourquoi ) (si la réponse est simplement "une version non atomique n'a tout simplement jamais été envisagée" ou "personne n'a jamais demandé une version non atomique" c'est très bien!).

Avec la question n ° 2, je me demande si quelqu'un a déjà proposé une version non atomique de shared_ptr(soit à Boost, soit au comité des normes) (non pas pour remplacer la version atomique de shared_ptr, mais pour coexister avec elle) et elle a été abattue pour un Raison spécifique.

Tiges de maïs
la source
4
De quel «coût» vous préoccupez-vous exactement ici? Le coût de l'incrémentation atomique d'un entier? Est-ce vraiment un coût qui vous préoccupe pour une application réelle? Ou êtes-vous simplement en train d'optimiser prématurément?
Nicol Bolas
9
@NicolBolas: C'est plus de la curiosité qu'autre chose; Je n'ai (actuellement) aucun code / projet pour lequel je souhaite sérieusement utiliser un pointeur partagé non atomique. Cependant, j'ai eu des projets (dans le passé) où Boost a shared_ptrété un ralentissement important en raison de son atomicité, et la définition a BOOST_DISABLE_THREADSfait une différence notable (je ne sais pas si std::shared_ptrcela aurait eu le même coût que cela boost::shared_ptr).
Cornstalks
13
@Close électeurs: quelle partie de la question n'est pas constructive? S'il n'y a pas de raison spécifique pour la deuxième question, c'est très bien (un simple «cela n'a tout simplement pas été pris en compte» serait une réponse suffisamment valable). Je suis curieux de savoir s'il existe une raison / une justification spécifique. Et la première question est certainement une question valable, je dirais. Si j'ai besoin de clarifier la question ou d'y apporter de légers ajustements, veuillez m'en informer. Mais je ne vois pas en quoi ce n'est pas constructif.
Cornstalks
10
@Cornstalks Eh bien, c'est probablement juste que les gens ne réagissent pas très bien aux questions qu'ils peuvent facilement qualifier d ' "optimisation prématurée" , quelle que soit la validité, la bonne pose ou la pertinence de la question, je suppose. Pour ma part, je ne vois aucune raison de fermer cela comme non constructif.
Christian Rau
13
(Impossible d'écrire une réponse maintenant, il est fermé, donc commenter) Avec GCC, lorsque votre programme n'utilise pas plusieurs threads, shared_ptrn'utilise pas d'opérations atomiques pour le refcount. Voir (2) sur gcc.gnu.org/ml/libstdc++/2007-10/msg00180.html pour un correctif à GCC pour permettre à l'implémentation non atomique d'être utilisée même dans les applications multithread, pour les shared_ptrobjets qui ne sont pas partagés entre fils. Je suis assis sur ce patch depuis des années mais j'envisage enfin de le valider pour GCC 4.9
Jonathan Wakely

Réponses:

104

1. Je me demande s'il existe une version non atomique de std :: shared_ptr disponible

Non fourni par la norme. Il peut bien y en avoir un fourni par une bibliothèque «tierce». En effet, avant C ++ 11 et avant Boost, il semblait que tout le monde écrivait sa propre référence comptant un pointeur intelligent (moi y compris).

2. Ma deuxième question est pourquoi une version non atomique de std :: shared_ptr n'a pas été fournie en C ++ 11?

Cette question a été débattue lors de la réunion de Rapperswil en 2010. Le sujet a été introduit par le commentaire n ° 20 d'un organisme national de la Suisse. Il y avait des arguments forts des deux côtés du débat, y compris ceux que vous fournissez dans votre question. Cependant, à la fin de la discussion, le vote était massivement (mais pas unanime) contre l'ajout d'une version non synchronisée (non atomique) de shared_ptr.

Les arguments contre comprenaient:

  • Le code écrit avec shared_ptr non synchronisé peut finir par être utilisé dans du code threadé plus tard, ce qui entraîne des problèmes difficiles à déboguer sans avertissement.

  • Avoir un shared_ptr "universel" qui est le "sens unique" du trafic dans le comptage des références présente des avantages: D'après la proposition d'origine :

    Possède le même type d'objet quelles que soient les fonctionnalités utilisées, ce qui facilite grandement l'interopérabilité entre les bibliothèques, y compris les bibliothèques tierces.

  • Le coût des atomes, bien que non nul, n'est pas écrasant. Le coût est atténué par l'utilisation de la construction de mouvement et de l'affectation de mouvement qui n'ont pas besoin d'utiliser des opérations atomiques. De telles opérations sont couramment utilisées pour l' vector<shared_ptr<T>>effacement et l'insertion.

  • Rien n'empêche les gens d'écrire leur propre pointeur intelligent compté par référence non atomique si c'est vraiment ce qu'ils veulent faire.

Le dernier mot du LWG de Rapperswil ce jour-là était:

Rejeter CH 20. Aucun consensus pour effectuer un changement pour le moment.

Howard Hinnant
la source
7
Wow, parfait, merci pour l'information! C'est exactement le genre d'informations que j'espérais trouver.
Cornstalks
> Has the same object type regardless of features used, greatly facilitating interoperability between libraries, including third-party libraries. c'est un raisonnement extrêmement étrange. Les bibliothèques tierces fourniront de toute façon leurs propres types, alors pourquoi serait-il important de les fournir sous la forme std :: shared_ptr <CustomType>, std :: non_atomic_shared_ptr <CustomType>, etc.? vous devrez toujours adapter votre code à ce que la bibliothèque renvoie de toute façon
Jean-Michaël Celerier
C'est vrai en ce qui concerne les types spécifiques aux bibliothèques, mais l'idée est qu'il existe également de nombreux endroits où les types standard apparaissent dans les API tierces. Par exemple, ma bibliothèque pourrait prendre un std::shared_ptr<std::string>quelque part. Si la bibliothèque de quelqu'un d'autre prend également ce type, les appelants peuvent nous transmettre les mêmes chaînes sans les inconvénients ou les frais généraux liés à la conversion entre différentes représentations, et c'est une petite victoire pour tout le monde.
Jack O'Connor
52

Howard a déjà bien répondu à la question, et Nicol a souligné les avantages d'avoir un seul type de pointeur partagé standard, plutôt que beaucoup de types incompatibles.

Bien que je sois entièrement d'accord avec la décision du comité, je pense qu'il y a un certain avantage à utiliser un shared_ptrtype non synchronisé dans des cas particuliers . J'ai donc étudié le sujet à quelques reprises.

Si je n'utilise pas plusieurs threads, ou si j'utilise plusieurs threads mais que je ne partage pas la propriété du pointeur entre les threads, un pointeur intelligent atomique est excessif.

Avec GCC lorsque votre programme n'utilise pas plusieurs threads shared_ptr n'utilise pas d'opérations atomiques pour le refcount. Cela se fait en mettant à jour le nombre de références via des fonctions wrapper qui détectent si le programme est multithread (sous GNU / Linux, cela se fait simplement en détectant si le programme est lié à libpthread.so) et en les distribuant aux opérations atomiques ou non atomiques en conséquence.

J'ai réalisé il y a de nombreuses années que parce que GCC shared_ptr<T>est implémenté en termes de __shared_ptr<T, _LockPolicy>classe de base , il est possible d'utiliser la classe de base avec la politique de verrouillage monothread même dans le code multithread, en utilisant explicitement __shared_ptr<T, __gnu_cxx::_S_single>. Malheureusement, comme ce n'était pas un cas d'utilisation prévu, cela ne fonctionnait pas de manière optimale avant GCC 4.9, et certaines opérations utilisaient toujours les fonctions wrapper et étaient donc distribuées aux opérations atomiques même si vous avez explicitement demandé la _S_singlepolitique. Voir le point (2) sur http://gcc.gnu.org/ml/libstdc++/2007-10/msg00180.htmlpour plus de détails et un correctif pour GCC pour permettre à l'implémentation non atomique d'être utilisée même dans les applications multithread. Je me suis assis sur ce patch pendant des années, mais je l'ai finalement validé pour GCC 4.9, qui vous permet d'utiliser un modèle d'alias comme celui-ci pour définir un type de pointeur partagé qui n'est pas thread-safe, mais qui est légèrement plus rapide:

template<typename T>
  using shared_ptr_unsynchronized = std::__shared_ptr<T, __gnu_cxx::_S_single>;

Ce type ne serait pas interopérable avec std::shared_ptr<T>et ne serait sûr à utiliser que s'il est garanti que les shared_ptr_unsynchronizedobjets ne seront jamais partagés entre les threads sans une synchronisation supplémentaire fournie par l'utilisateur.

C'est bien sûr complètement non portable, mais parfois c'est OK. Avec les bons hacks de préprocesseur, votre code fonctionnera toujours bien avec d'autres implémentations si shared_ptr_unsynchronized<T>c'est un alias pour shared_ptr<T>, ce serait juste un peu plus rapide avec GCC.


Si vous utilisez un GCC antérieur à 4.9, vous pouvez l'utiliser en ajoutant les _Sp_counted_base<_S_single>spécialisations explicites à votre propre code (et en vous assurant que personne n'instancie jamais __shared_ptr<T, _S_single>sans inclure les spécialisations, pour éviter les violations ODR.) L'ajout de telles spécialisations de stdtypes est techniquement indéfini, mais le ferait travailler dans la pratique, car dans ce cas, il n'y a aucune différence entre moi ajoutant les spécialisations à GCC ou vous les ajoutant à votre propre code.

Jonathan Wakely
la source
2
Vous vous demandez simplement, y a-t-il une faute de frappe dans votre exemple d'alias de modèle? Ie je pense qu'il devrait lire shared_ptr_unsynchronized = std :: __ shared_ptr <. D'ailleurs, j'ai testé cela aujourd'hui, en conjonction avec std :: __ enable_shared_from_this et std :: __ low_ptr, et cela semble fonctionner correctement (gcc 4.9 et gcc 5.2). Je vais le profiler / le démonter sous peu pour voir si effectivement les opérations atomiques sont ignorées.
Carl Cook
Super détails! Récemment , je fait face à un problème, comme décrit dans cette question , qui a finalement m'a fait de regarder dans le code source de std::shared_ptr, std::__shared_ptr, __default_lock_policyet tel. Cette réponse a confirmé ce que j'ai compris du code.
Nawaz
21

Ma deuxième question est pourquoi une version non atomique de std :: shared_ptr n'a pas été fournie en C ++ 11? (en supposant qu'il y ait un pourquoi).

On pourrait tout aussi facilement se demander pourquoi il n'y a pas de pointeur intrusif, ou un certain nombre d'autres variantes possibles de pointeurs partagés que l'on pourrait avoir.

La conception de shared_ptr, transmise par Boost, a été de créer une lingua-franca standard minimale de pointeurs intelligents. En règle générale, vous pouvez simplement retirer cela du mur et l'utiliser. C'est quelque chose qui serait utilisé en général, dans une grande variété d'applications. Vous pouvez le mettre dans une interface, et il y a de fortes chances que de bonnes personnes soient disposées à l'utiliser.

Le filetage ne fera que devenir plus répandu à l'avenir. En effet, au fil du temps, le filetage sera généralement l'un des principaux moyens d'atteindre les performances. Exiger que le pointeur intelligent de base fasse le strict minimum nécessaire pour prendre en charge le threading facilite cette réalité.

Jeter une demi-douzaine de pointeurs intelligents avec des variations mineures entre eux dans la norme, ou pire encore un pointeur intelligent basé sur une politique, aurait été terrible. Chacun choisirait le pointeur qu'il préfère et renoncerait à tous les autres. Personne ne pourrait communiquer avec qui que ce soit. Ce serait comme les situations actuelles avec les chaînes C ++, où chacun a son propre type. Seulement bien pire, car l'interopérabilité avec des chaînes est beaucoup plus facile que l'interopérabilité entre les classes de pointeurs intelligents.

Boost, et par extension le comité, a choisi un pointeur intelligent spécifique à utiliser. Il offrait un bon équilibre des fonctionnalités et était largement et couramment utilisé dans la pratique.

std::vectora quelques inefficacités par rapport aux tableaux nus dans certains cas aussi. Il a certaines limites; certaines utilisations veulent vraiment avoir une limite stricte sur la taille de a vector, sans utiliser d'allocateur de lancement. Cependant, le comité n'a pas conçu vectorpour être tout pour tout le monde. Il a été conçu pour être un bon défaut pour la plupart des applications. Ceux pour qui cela ne peut pas fonctionner peuvent simplement écrire une alternative qui répond à leurs besoins.

Tout comme vous pouvez pour un pointeur intelligent si shared_ptrl'atomicité de est un fardeau. Là encore, on pourrait aussi envisager de ne pas trop les copier.

Nicol Bolas
la source
7
+1 pour "on pourrait aussi envisager de ne pas trop les copier."
Ali
Si jamais vous connectez un profileur, vous êtes spécial et vous pouvez simplement supprimer des arguments comme ci-dessus. Si vous n'avez pas d'exigences opérationnelles difficiles à satisfaire, vous ne devez pas utiliser C ++. Se disputer comme vous est un bon moyen de rendre le C ++ universellement vilipendé par quiconque s'intéresse aux hautes performances ou à la faible latence.C'est pourquoi les programmeurs de jeux n'utilisent pas STL, boost ou même des exceptions.
Hans Malherbe
Pour plus de clarté, je pense que la citation en haut de votre réponse devrait se lire "pourquoi une version non atomique de std :: shared_ptr n'a-t-elle pas été fournie dans C ++ 11?"
Charles Savoie
4

Je prépare une conférence sur shared_ptr au travail. J'ai utilisé un boost shared_ptr modifié avec éviter malloc séparé (comme ce que make_shared peut faire) et un paramètre de modèle pour la politique de verrouillage comme shared_ptr_unsynchronized mentionné ci-dessus. J'utilise le programme depuis

http://flyingfrogblog.blogspot.hk/2011/01/boosts-sharedptr-up-to-10-slower-than.html

comme test, après avoir nettoyé les copies shared_ptr inutiles. Le programme utilise uniquement le thread principal et l'argument de test est affiché. L'env de test est un notebook exécutant linuxmint 14. Voici le temps pris en secondes:

test run setup boost (1.49) std avec boost modifié make_shared
mt-unsafe (11) 11,9 9 / 11,5 (-pthread activé) 8,4  
atomique (11) 13,6 12,4 13,0  
mt-unsafe (12) 113,5 85,8 / 108,9 (-pthread on) 81,5  
atomique (12) 126,0 109,1 123,6  

Seule la version 'std' utilise -std = cxx11, et -pthread bascule probablement lock_policy dans la classe g ++ __shared_ptr.

À partir de ces chiffres, je vois l'impact des instructions atomiques sur l'optimisation du code. Le scénario de test n'utilise aucun conteneur C ++, mais vector<shared_ptr<some_small_POD>>risque de souffrir si l'objet n'a pas besoin de la protection des threads. Boost souffre moins probablement parce que le malloc supplémentaire limite la quantité d'inlining et d'optimisation du code.

Je n'ai pas encore trouvé de machine avec suffisamment de cœurs pour tester l'évolutivité des instructions atomiques, mais utiliser std :: shared_ptr uniquement lorsque cela est nécessaire est probablement mieux.

russ
la source
3

Boost fournit un système shared_ptrnon atomique. Il s'appelle local_shared_ptret se trouve dans la bibliothèque de pointeurs intelligents de boost.

Le physicien quantique
la source
+1 pour une réponse courte et solide avec une bonne citation, mais ce type de pointeur semble coûteux - en termes de mémoire et d'exécution, en raison d'un niveau supplémentaire d'indirection (local-> shared-> ptr vs shared-> ptr).
Red.Wave
@ Red.Wave Pouvez-vous expliquer ce que vous entendez par indirection et comment cela affecte les performances? Voulez-vous dire que c'est shared_ptrun comptoir de toute façon, même si c'est local? Ou voulez-vous dire qu'il y a un autre problème avec ça? La documentation dit que la seule différence est que ce n'est pas atomique.
The Quantum Physicist
Chaque ptr local garde un compte et une référence au ptr partagé d'origine. Ainsi, tout accès à la pointe finale nécessite une dérogation du ptr local au point partagé, qui est alors une dérogation à la pointee. Ainsi, il y a une autre indirection empilée jusqu'aux indirections de ptr partagé. Et cela augmente les frais généraux.
Red.Wave
@ Red.Wave D'où proviennent ces informations? Ceci: "Ainsi, tout accès au point final a besoin d'une dérogation du local au ptr partagé" mérite d'être cité. Je n'ai pas pu trouver cela dans les documents boost. Encore une fois, ce que j'ai vu dans la documentation, c'est qu'il dit cela local_shared_ptret qu'ils shared_ptrsont identiques sauf pour atomic. Je suis vraiment intéressé à savoir si ce que vous dites est vrai parce que je l'utilise local_shared_ptrdans des applications qui nécessitent des performances élevées.
The Quantum Physicist
3
@ Red.Wave Si vous regardez le code source réel github.com/boostorg/smart_ptr/blob/ ... vous verrez qu'il n'y a pas de double indirection. Ce paragraphe de la documentation n'est qu'un modèle mental.
Ilya Popov