web-dev-qa-db-fra.com

Le coût du passage par shared_ptr

J'utilise beaucoup std :: tr1 :: shared_ptr dans toute mon application. Cela inclut le passage d'objets en tant qu'arguments de fonction. Considérer ce qui suit:

class Dataset {...}

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

Alors que le passage d'un objet de jeu de données via shared_ptr garantit son existence à l'intérieur de 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 plat gprof d'une exécution récente:

 Chaque échantillon compte pour 0,01 seconde. 
% Auto total cumulé 
 Temps secondes secondes appels s/appel s/nom de l'appel 
 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, environ 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 monothread et je pensais réécrire certaines des fonctions comme

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

et remplacer les appels

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

avec

f( *pds );

dans des endroits où je sais avec certitude que l'objet ne sera pas détruit tant que le flux du programme est à l'intérieur de f (). Mais avant de m'enfuir pour changer un tas de signatures/appels de fonctions, je voulais savoir quel était le résultat typique du passage par shared_ptr. Il semble que shared_ptr ne devrait pas être utilisé pour les fonctions appelées très souvent.

Toute contribution serait appréciée. Merci d'avoir lu.

-Artem

pdate: Après avoir changé une poignée de fonctions pour accepter const Dataset&, le nouveau profil ressemble à ceci:

 Chaque échantillon compte pour 0,01 seconde. 
% Auto total cumulé 
 Temps secondes secondes appels s/appel s/nom de l'appel 
 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 &) 

Je suis un peu perplexe devant le nombre d'appels de destructeurs inférieurs au nombre d'appels de constructeur de copie, mais dans l'ensemble, je suis très satisfait de la diminution du temps d'exécution associé. Merci à tous pour leurs conseils.

57
Artem Sokolov

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) {...} 

Edit: Concernant les problèmes de sécurité mentionnés par d'autres:

  • Lors de l'utilisation de shared_ptr fortement dans une application, passer par valeur prendra énormément de temps (je l'ai vu aller 50 +%).
  • Utilisation const T& au lieu de const shared_ptr<T const>& lorsque l'argument ne doit pas être nul.
  • En utilisant const shared_ptr<T const>& est plus sûr que const T* lorsque les performances sont un problème.
56
Sam Harwell

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

10
Alex F

Si vous n'utilisez pas make_shared , pourriez-vous essayer? En localisant le nombre de références et l'objet dans la même zone de mémoire, vous pouvez voir un gain de performance associé à la cohérence du cache. Ça vaut le coup d'essayer de toute façon.

5
Kylotan

Toute création et destruction d'objets, en particulier la création et la destruction d'objets redondants, doit être évitée dans les applications à performances 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 complètement, ce qui va être cauchemardesque sur votre cache.

Vous avez probablement besoin du shared_ptr (car si vous pouviez vous en tirer avec un objet local, vous n'en alloueriez pas un hors du tas), mais vous pourriez même "mettre en cache" le résultat de la déréférence 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 plus de mémoire que ce qui est absolument nécessaire.

3
dash-tom-bang

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 qu'appeler le constructeur à un pointeur de comptage de références ne coûte cher que si vous le faites constamment.

Le seul avertissement que je puisse vous donner est: 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 autre (ptr) et autre fait un pointeur partagé du pointeur brut. Lorsque le nombre de références de ce deuxième 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 des pointeurs de comptage de références, vous devez donc faire attention aux cas d'angle comme celui-là.

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

1
Chris H