57 votes

Le coût du passage par shared_ptr

J'utilise std::tr1::shared_ptr de manière intensive dans mon application. Cela inclut le passage d'objets en tant qu'arguments de fonction. Prenons l'exemple suivant :

class Dataset {...}

void f( shared_ptr< Dataset const > pds ) {...}
void g( shared_ptr< Dataset const > pds ) {...}
...

Bien que le passage d'un objet dataset via shared_ptr garantisse son existence dans f et g, les fonctions peuvent être appelées des millions de fois, ce qui entraîne la création et la destruction de nombreux objets shared_ptr. Voici un extrait du profil gprof plat d'une exécution récente :

Each sample counts as 0.01 seconds.
  %   cumulative   self              self     total
 time   seconds   seconds    calls   s/call   s/call  name
  9.74    295.39    35.12 2451177304     0.00     0.00  std::tr1::\_\_shared\_count::\_\_shared\_count(std::tr1::\_\_shared\_count const&)
  8.03    324.34    28.95 2451252116     0.00     0.00  std::tr1::\_\_shared\_count::~\_\_shared\_count()

Ainsi, ~17% du temps d'exécution a été consacré au comptage de références avec des objets shared_ptr. Est-ce normal ?

Une grande partie de mon application est monofilière et je pensais réécrire certaines des fonctions en tant que

void f( const Dataset& ds ) {...}

et en remplaçant les appels

shared_ptr< Dataset > pds( new Dataset(...) );
f( pds );

avec

f( *pds );

à des endroits où je suis sûr que l'objet ne sera pas détruit alors que le flux du programme se trouve dans f(). Mais avant de me précipiter pour changer un tas de signatures de fonctions / appels, je voulais savoir quel était l'impact typique sur les performances du passage par shared_ptr. Il semble que shared_ptr ne devrait pas être utilisé pour les fonctions qui sont appelées très souvent.

Tout commentaire serait apprécié. Merci de votre lecture.

-Artem

Mise à jour : Après avoir modifié une poignée de fonctions pour accepter const Dataset& le nouveau profil ressemble à ceci :

Each sample counts as 0.01 seconds.
  %   cumulative   self              self     total
 time   seconds   seconds    calls   s/call   s/call  name
  0.15    241.62     0.37 24981902     0.00     0.00  std::tr1::\_\_shared\_count::~\_\_shared\_count()
  0.12    241.91     0.30 28342376     0.00     0.00  std::tr1::\_\_shared\_count::\_\_shared\_count(std::tr1::\_\_shared\_count const&)

Le fait que le nombre d'appels au destructeur soit inférieur au nombre d'appels à la copie du constructeur me laisse un peu perplexe, mais je suis globalement très satisfait de la diminution du temps d'exécution associé. Merci à tous pour leurs conseils.

57voto

280Z28 Points 49515

Passez toujours votre shared_ptr par const référence :

void f(const shared_ptr<Dataset const>& pds) {...} 
void g(const shared_ptr<Dataset const>& pds) {...} 

Editar: En ce qui concerne les problèmes de sécurité mentionnés par d'autres :

  • Lorsque vous utilisez shared_ptr Dans l'ensemble d'une application, le passage par valeur prendra énormément de temps (j'ai vu que cela représentait plus de 50 %).
  • Utilice const T& au lieu de const shared_ptr<T const>& lorsque l'argument ne doit pas être nul.
  • Utilisation de const shared_ptr<T const>& est plus sûr que const T* lorsque la performance est un problème.

11voto

Alex Farber Points 19387

Vous n'avez besoin du shared_ptr que pour le passer aux fonctions/objets qui le conservent pour une utilisation future. Par exemple, certaines classes peuvent conserver les shared_ptr pour les utiliser dans un worker thread. Pour les appels synchrones simples, il est tout à fait suffisant d'utiliser un simple pointeur ou une référence. shared_ptr ne devrait pas remplacer complètement l'utilisation de pointeurs simples.

4voto

Kylotan Points 14114

Si vous n'utilisez pas faire_partager pourriez-vous essayer ? En plaçant le nombre de références et l'objet dans la même zone de la mémoire, vous puede voir un gain de performance associé à la cohérence du cache. Cela vaut quand même la peine d'essayer.

3voto

dash-tom-bang Points 9384

Toute création et destruction d'objet, en particulier la création et la destruction d'objets redondants, doit être évitée dans les applications dont les performances sont critiques.

Considérez ce que fait shared_ptr. Non seulement il crée un nouvel objet et le remplit, mais il fait également référence à l'état partagé pour incrémenter les informations de référence, et l'objet lui-même vit probablement ailleurs, ce qui va être cauchemardesque pour votre cache.

On peut supposer que vous avez besoin du shared_ptr (parce que si vous pouviez vous en sortir avec un objet local, vous n'en alloueriez pas un à partir du tas), mais vous pourriez même "cacher" le résultat de la déréférence du shared_ptr :

void fn(shared_ptr< Dataset > pds)
{
   Dataset& ds = *pds;

   for (i = 0; i < 1000; ++i)
   {
      f(ds);
      g(ds);
   }
}

...parce que même *pds nécessite de frapper plus de mémoire que ce qui est absolument nécessaire.

1voto

Chris H Points 3534

On dirait que vous savez vraiment ce que vous faites. Vous avez profilé votre application, et vous savez exactement où les cycles sont utilisés. Vous comprenez que l'appel au constructeur d'un pointeur de comptage de références n'est coûteux que si vous le faites constamment.

Le seul conseil que je peux vous donner est le suivant : supposons qu'à l'intérieur de la fonction f(t *ptr), si vous appelez une autre fonction qui utilise des pointeurs partagés, et que vous faites other(ptr) et other fait un pointeur partagé du pointeur brut. Lorsque le nombre de références de ce second pointeur partagé atteint 0, vous avez effectivement supprimé votre objet...., même si vous ne le vouliez pas. Vous avez dit que vous utilisiez beaucoup les pointeurs avec comptage de références, vous devez donc faire attention à ce genre de cas.

EDIT : Vous pouvez rendre le destructeur privé, et seulement un ami de la classe de pointeur partagé, de sorte que le destructeur ne peut être appelé que par un pointeur partagé, alors vous êtes en sécurité. N'empêche pas les suppressions multiples de pointeurs partagés. Conformément au commentaire de Mat.

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