web-dev-qa-db-fra.com

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

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

1. Autant que je sache, std::shared_ptr est le seul pointeur intelligent dans <memory> c'est atomique. Je me demande s'il existe une version non atomique de std::shared_ptr disponible (je ne vois rien dans <memory>, donc je suis également ouvert aux suggestions en dehors de la norme, comme celles de Boost). Je connais boost::shared_ptr est également atomique (si BOOST_SP_DISABLE_THREADS n'est pas défini), mais peut-être y a-t-il une autre alternative? Je cherche quelque chose qui a la même sémantique que std::shared_ptr, mais sans l'atomicité.

2. Je comprends pourquoi std::shared_ptr est atomique; c'est plutôt sympa. Cependant, ce n'est pas agréable dans toutes les situations, et C++ a toujours eu le mantra de "ne payer que 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 exagéré. Ma deuxième question est pourquoi n'était pas une version non atomique de std::shared_ptr fourni en C++ 11 ? (en supposant qu'il y a pourquoi ) (si la réponse est simplement "une version non atomique n'a simplement jamais été envisagée" ou "personne n'a jamais demandé version non atomique "ça va!).

Avec la question # 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) (ne pas remplacer la version atomique de shared_ptr, mais pour y coexister) et il a été abattu pour une raison précise.

83
Cornstalks

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

Non fourni par la norme. Il pourrait bien y en avoir une fournie par une bibliothèque "tierce". En effet, avant C++ 11 et avant Boost, il semblait que tout le monde écrivait son propre pointeur intelligent compté par référence (y compris moi-même).

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

Cette question a été discutée lors de la réunion de Rapperswil en 2010. Le sujet a été introduit par un commentaire de l'organisme national n ° 20 de la Suisse. Il y avait des arguments solides 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 a été massivement (mais pas unanime) contre l'ajout d'une version non synchronisée (non atomique) de shared_ptr.

Arguments contre inclus:

  • Le code écrit avec le shared_ptr non synchronisé peut finir par être utilisé dans le code threadé sur la route, finissant par causer des problèmes de débogage difficiles sans avertissement.

  • Avoir un shared_ptr "universel" qui est "à sens unique" pour le trafic dans le comptage des références présente des avantages: De la proposition d'origine :

    Possède le même type d'objet indépendamment des fonctionnalités utilisées, facilitant considérablement l'interopérabilité entre les bibliothèques, y compris les bibliothèques tierces.

  • Le coût de l'atome, bien qu'il ne soit pas nul, n'est pas écrasant. Le coût est atténué par l'utilisation de la construction et de l'affectation des mouvements qui n'ont pas besoin d'utiliser des opérations atomiques. De telles opérations sont couramment utilisées dans vector<shared_ptr<T>> effacez et insérez.

  • Rien n'interdit aux gens d'écrire leur propre pointeur intelligent à comptage de références non atomiques si c'est vraiment ce qu'ils veulent faire.

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

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

101
Howard Hinnant

Howard a déjà bien répondu à la question, et Nicol a fait de bons arguments sur les avantages d'avoir un seul type de pointeur partagé standard, plutôt que de nombreux incompatibles.

Bien que je sois entièrement d'accord avec la décision du comité, je pense qu'il y a un avantage à utiliser un type non synchronisé de type shared_ptr dans des cas spéciaux , j'ai donc étudié le sujet à plusieurs 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 exagéré.

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 les décomptes de références via des fonctions wrapper qui détectent si le programme est multithread (sur GNU/Linux cela se fait simplement en détectant si le programme est lié à libpthread.so) Et en envoyant aux opérations atomiques ou non atomiques en conséquence.

J'ai réalisé il y a de nombreuses années que parce que shared_ptr<T> De GCC 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 à un seul thread même en code multithread, en utilisant explicitement __shared_ptr<T, __gnu_cxx::_S_single>. Malheureusement, car ce n'était pas un cas d'utilisation prévu, cela ne fonctionnait pas tout à fait de manière optimale avant GCC 4.9, et certaines opérations utilisaient toujours les fonctions d'encapsulation et étaient donc envoyées aux opérations atomiques même si vous aviez explicitement demandé la politique _S_single . Voir le point (2) sur http://gcc.gnu.org/ml/libstdc++/2007-10/msg00180.html pour plus de détails et un correctif pour GCC pour permettre à l'implémentation non atomique de être utilisé même dans les applications multithread. Je me suis assis sur ce correctif 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 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 objets shared_ptr_unsynchronized Ne seraient jamais partagés entre les threads sans 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 du préprocesseur, votre code fonctionnerait toujours bien avec d'autres implémentations si shared_ptr_unsynchronized<T> Est un alias pour shared_ptr<T>, Ce serait juste un peu plus rapide avec GCC.


Si vous utilisez un GCC avant 4.9, vous pouvez l'utiliser en ajoutant les spécialisations explicites _Sp_counted_base<_S_single> À votre propre code (et en veillant à ce 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 types std est techniquement indéfini, mais fonctionnerait dans la pratique, car dans ce cas, il n'y a pas de différence entre l'ajout des spécialisations à GCC ou vous les ajoutez à votre propre code.

49
Jonathan Wakely

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

On pourrait tout aussi bien 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. Que, d'une manière générale, vous pouvez simplement retirer ceci du mur et l'utiliser. C'est quelque chose qui serait utilisé de manière générale, dans une grande variété d'applications. Vous pouvez le mettre dans une interface, et les chances sont bonnes que les gens soient prêts à l'utiliser.

Le filetage va seulement 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 filetage facilite cette réalité.

Vider une demi-douzaine de pointeurs intelligents avec des variations mineures entre eux dans la norme, ou pire encore un pointeur intelligent basé sur des politiques, aurait été terrible. Chacun choisirait le pointeur qu'il préfère et renoncerait à tous les autres. Personne ne pourrait communiquer avec quelqu'un d'autre. Ce serait comme les situations actuelles avec des chaînes C++, où chacun a son propre type. Pire encore, 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é ont 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::vector présente également des inefficacités par rapport aux tableaux nus dans certains cas d'angle. Il a certaines limites; certaines utilisations veulent vraiment avoir une limite stricte sur la taille d'un vector, sans utiliser d'allocateur de lancement. Cependant, le comité n'a pas conçu vector pour être tout pour tout le monde. Il a été conçu pour être une bonne valeur par défaut pour la plupart des applications. Ceux pour qui cela ne peut pas fonctionner peuvent simplement écrire une alternative adaptée à leurs besoins.

Tout comme vous pouvez pour un pointeur intelligent si l'atomicité de shared_ptr est un fardeau. Là encore, on pourrait également envisager de ne pas les copier autant.

22
Nicol Bolas

Je prépare un exposé sur shared_ptr au travail. J'ai utilisé un boost modifié shared_ptr 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 de

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é. Le test env est un ordinateur portable exécutant linuxmint 14. Voici le temps pris en secondes:

 test run setup boost (1.49) std with make_shared modified boost 
 mt-unsafe (11) 11.9 9/11.5 (-pthread on) 8.4 
 atomic (11) 13.6 12.4 13.0 
 mt-unsafe (12) 113,5 85,8/108,9 (-pthread on) 81,5 
 atomic (12) 126,0 109,1 123,6 

Seule la version 'std' utilise -std = cxx11, et -pthread commute 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>> est susceptible de souffrir si l'objet n'a pas besoin de la protection du thread. Boost souffre moins probablement parce que le malloc supplémentaire limite la quantité d'inline et d'optimisation de code.

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

4
russ

Boost fournit un shared_ptr c'est non atomique. C'est appelé local_shared_ptr , et peut être trouvé dans la bibliothèque de pointeurs intelligents de boost.

3