Quelle est la surcharge des pointeurs intelligents par rapport aux pointeurs normaux en C ++?

102

Quelle est la surcharge des pointeurs intelligents par rapport aux pointeurs normaux en C ++ 11? En d'autres termes, mon code sera-t-il plus lent si j'utilise des pointeurs intelligents, et si oui, combien plus lent?

Plus précisément, je pose des questions sur le C ++ 11 std::shared_ptret std::unique_ptr.

De toute évidence, les éléments poussés vers le bas de la pile seront plus grands (du moins je le pense), car un pointeur intelligent doit également stocker son état interne (nombre de références, etc.), la question est vraiment de savoir combien cela va-t-il affecter mes performances, voire pas du tout?

Par exemple, je renvoie un pointeur intelligent à partir d'une fonction au lieu d'un pointeur normal:

std::shared_ptr<const Value> getValue();
// versus
const Value *getValue();

Ou, par exemple, lorsqu'une de mes fonctions accepte un pointeur intelligent comme paramètre au lieu d'un pointeur normal:

void setValue(std::shared_ptr<const Value> val);
// versus
void setValue(const Value *val);
Venemo
la source
8
Le seul moyen de le savoir est de comparer votre code.
Basile Starynkevitch
Lequel voulez-vous dire? std::unique_ptrou std::shared_ptr?
stefan
10
La réponse est 42. (un autre mot, qui sait, vous devez profiler votre code et comprendre sur votre matériel pour votre charge de travail typique.)
Nim
Votre application doit faire un usage extrême des pointeurs intelligents pour qu'elle soit significative.
user2672165
Le coût d'utilisation d'un shared_ptr dans une simple fonction setter est terrible et ajoutera une surcharge multiple de 100%.
Lothar

Réponses:

178

std::unique_ptr a une surcharge de mémoire uniquement si vous lui fournissez un suppresseur non trivial.

std::shared_ptr a toujours une surcharge de mémoire pour le compteur de référence, même si elle est très petite.

std::unique_ptr a une surcharge de temps uniquement pendant le constructeur (s'il doit copier le suppresseur fourni et / ou initialiser le pointeur par null) et pendant le destructeur (pour détruire l'objet possédé).

std::shared_ptra une surcharge de temps dans le constructeur (pour créer le compteur de référence), dans le destructeur (pour décrémenter le compteur de référence et éventuellement détruire l'objet) et dans l'opérateur d'affectation (pour incrémenter le compteur de référence). En raison des garanties de sécurité des threads std::shared_ptr, ces incréments / décréments sont atomiques, ajoutant ainsi un peu plus de temps système.

Notez qu'aucun d'entre eux n'a de surcharge de temps pour le déréférencement (pour obtenir la référence à l'objet possédé), alors que cette opération semble être la plus courante pour les pointeurs.

Pour résumer, il y a une surcharge, mais cela ne devrait pas ralentir le code, sauf si vous créez et détruisez continuellement des pointeurs intelligents.

lisyarus
la source
11
unique_ptrn'a pas de frais généraux dans le destructeur. Il fait exactement la même chose que vous le feriez avec un pointeur brut.
R. Martinho Fernandes
6
@ R.MartinhoFernandes comparé au pointeur brut lui-même, il a une surcharge de temps dans le destructeur, car le destructeur de pointeur brut ne fait rien. Comparé à la façon dont un pointeur brut serait probablement utilisé, il n'a sûrement pas de surcharge.
lisyarus
3
Il convient de noter qu'une partie du coût de construction / destruction / affectation shared_ptr est due à la sécurité des threads
Joe
1
Et qu'en est-il du constructeur par défaut de std::unique_ptr? Si vous construisez a std::unique_ptr<int>, l'interne int*est initialisé selon nullptrque vous le vouliez ou non.
Martin Drozdik
1
@MartinDrozdik Dans la plupart des situations, vous initialiseriez également le pointeur brut par null, pour vérifier sa nullité plus tard, ou quelque chose comme ça. Néanmoins, ajouté ceci à la réponse, merci.
lisyarus
26

Comme pour toutes les performances de code, le seul moyen vraiment fiable d'obtenir des informations concrètes est de mesurer et / ou d' inspecter le code machine.

Cela dit, un raisonnement simple dit que

  • Vous pouvez vous attendre à une surcharge dans les versions de débogage, puisque par exemple operator->doit être exécuté comme un appel de fonction afin que vous puissiez y entrer (ceci est à son tour dû au manque général de support pour marquer les classes et les fonctions comme non-débogage).

  • Car shared_ptrvous pouvez vous attendre à une surcharge lors de la création initiale, car cela implique l'allocation dynamique d'un bloc de contrôle, et l'allocation dynamique est beaucoup plus lente que toute autre opération de base en C ++ (à utiliser make_sharedlorsque cela est pratiquement possible, pour minimiser cette surcharge).

  • De plus, shared_ptril y a une surcharge minime pour maintenir un décompte de références, par exemple lors du passage d'une shared_ptrvaleur par, mais il n'y a pas une telle surcharge pour unique_ptr.

En gardant à l'esprit le premier point ci-dessus, lorsque vous mesurez, faites cela à la fois pour les versions de débogage et de publication.

Le comité international de normalisation C ++ a publié un rapport technique sur les performances , mais c'était en 2006, avant unique_ptret shared_ptront été ajoutés à la bibliothèque standard. Pourtant, les pointeurs intelligents étaient vieux chapeau à ce moment-là, donc le rapport en a également tenu compte. Citant la partie pertinente:

«Si l'accès à une valeur via un pointeur intelligent trivial est nettement plus lent que d'y accéder via un pointeur ordinaire, le compilateur gère l'abstraction de manière inefficace. Dans le passé, la plupart des compilateurs avaient des pénalités d'abstraction importantes et plusieurs compilateurs actuels le font toujours. Cependant, au moins deux compilateurs auraient des pénalités d'abstraction inférieures à 1% et un autre une pénalité de 3%, donc l'élimination de ce type de frais généraux est tout à fait dans l'état de la technique "

En tant que supposition éclairée, le «bien dans l'état de l'art» a été atteint avec les compilateurs les plus populaires aujourd'hui, au début de 2014.

Bravo et hth. - Alf
la source
Pourriez-vous s'il vous plaît inclure quelques détails dans votre réponse sur les cas que j'ai ajoutés à ma question?
Venemo
Cela aurait pu être vrai il y a 10 ans ou plus, mais aujourd'hui, l'inspection du code de la machine n'est pas aussi utile que la personne ci-dessus le suggère. Selon la façon dont les instructions sont pipelinées, vectorisées, ... et comment le compilateur / processeur traite la spéculation en fin de compte, c'est à quelle vitesse elle est. Moins de code machine ne signifie pas nécessairement un code plus rapide. La seule façon de déterminer les performances est de les profiler. Cela peut changer sur la base du processeur et également par compilateur.
Byron le
Un problème que j'ai vu est que, une fois que shared_ptrs est utilisé dans un serveur, alors l'utilisation de shared_ptrs commence à proliférer et bientôt shared_ptrs devient la technique de gestion de la mémoire par défaut. Alors maintenant, vous avez répété des pénalités d'abstraction de 1 à 3% qui sont reprises encore et encore.
Nathan Doromal
Je pense que l'analyse comparative d'une version de débogage est une perte de temps totale
Paul Childs
26

Ma réponse est différente des autres et je me demande vraiment s'ils ont déjà profilé du code.

shared_ptr a une surcharge importante pour la création en raison de son allocation de mémoire pour le bloc de contrôle (qui conserve le compteur de références et une liste de pointeurs vers toutes les références faibles). Il a également une surcharge de mémoire énorme à cause de cela et du fait que std :: shared_ptr est toujours un tuple à 2 pointeurs (un vers l'objet, un vers le bloc de contrôle).

Si vous passez un shared_pointer à une fonction en tant que paramètre de valeur, il sera au moins 10 fois plus lent qu'un appel normal et créera beaucoup de codes dans le segment de code pour le déroulement de la pile. Si vous le passez par référence, vous obtenez une indirection supplémentaire qui peut également être bien pire en termes de performances.

C'est pourquoi vous ne devriez pas faire cela à moins que la fonction ne soit vraiment impliquée dans la gestion de la propriété. Sinon, utilisez "shared_ptr.get ()". Il n'est pas conçu pour garantir que votre objet n'est pas tué lors d'un appel de fonction normal.

Si vous devenez fou et utilisez shared_ptr sur de petits objets comme une arborescence de syntaxe abstraite dans un compilateur ou sur de petits nœuds dans toute autre structure de graphe, vous verrez une énorme baisse de performances et une énorme augmentation de la mémoire. J'ai vu un système d'analyse syntaxique qui a été réécrit peu de temps après l'arrivée de C ++ 14 sur le marché et avant que le programmeur n'apprenne à utiliser correctement les pointeurs intelligents. La réécriture était une magnitude plus lente que l'ancien code.

Ce n'est pas une solution miracle et les pointeurs bruts ne sont pas non plus mauvais par définition. Les mauvais programmeurs sont mauvais et la mauvaise conception est mauvaise. Concevez avec soin, concevez avec une propriété claire à l'esprit et essayez d'utiliser le shared_ptr principalement sur la limite de l'API du sous-système.

Si vous voulez en savoir plus , vous pouvez regarder M. Nicolai Josuttis bien parler de « Le prix réel de Pointeurs partagé en C ++ » https://vimeo.com/131189627
Il va profondément dans l'architecture des détails de mise en œuvre et de la CPU pour les barrières d'écriture, atomique serrures, etc. une fois que vous écoutez, vous ne parlerez jamais de cette fonctionnalité étant bon marché. Si vous voulez juste une preuve de la magnitude plus lente, ignorez les 48 premières minutes et regardez-le exécuter un exemple de code qui s'exécute jusqu'à 180 fois plus lentement (compilé avec -O3) lorsque vous utilisez un pointeur partagé partout.

Lothar
la source
Merci pour votre réponse! Sur quelle plateforme avez-vous profilé? Pouvez-vous sauvegarder vos réclamations avec des données?
Venemo
Je n'ai pas de numéro à afficher, mais vous pouvez en trouver dans Nico Josuttis talk vimeo.com/131189627
Lothar
6
Jamais entendu parler std::make_shared()? De plus, je trouve que les démonstrations d'abus flagrant sont un peu ennuyeuses ...
Deduplicator
2
Tout ce que "make_shared" peut faire est de vous mettre à l'abri d'une allocation supplémentaire et de vous donner un peu plus de localité de cache si le bloc de contrôle est alloué devant l'objet. Cela ne peut pas du tout aider lorsque vous passez le pointeur. Ce n'est pas la racine des problèmes.
Lothar
14

En d'autres termes, mon code sera-t-il plus lent si j'utilise des pointeurs intelligents, et si oui, combien plus lent?

Ralentissez? Probablement pas, à moins que vous ne créiez un énorme index à l'aide de shared_ptrs et que vous n'ayez pas assez de mémoire au point que votre ordinateur commence à se froisser, comme une vieille dame tombée au sol par une force insupportable de loin.

Ce qui ralentirait votre code, ce sont des recherches lentes, un traitement de boucle inutile, d'énormes copies de données et de nombreuses opérations d'écriture sur le disque (comme des centaines).

Les avantages d'un pointeur intelligent sont tous liés à la gestion. Mais les frais généraux sont-ils nécessaires? Cela dépend de votre implémentation. Disons que vous itérez sur un tableau de 3 phases, chaque phase a un tableau de 1024 éléments. La création d'un smart_ptrpour ce processus peut être exagéré, car une fois l'itération terminée, vous saurez que vous devez l'effacer. Ainsi, vous pourriez gagner de la mémoire supplémentaire en n'utilisant pas un smart_ptr...

Mais tu veux vraiment faire ça?

Une seule fuite de mémoire pourrait faire en sorte que votre produit ait un point de défaillance dans le temps (disons que votre programme fuit 4 mégaoctets par heure, il faudrait des mois pour casser un ordinateur, néanmoins, il se cassera, vous le savez car la fuite est là) .

C'est comme dire "votre logiciel est garanti 3 mois, alors appelez-moi pour le service".

Donc, en fin de compte, c'est vraiment une question de ... pouvez-vous gérer ce risque? utiliser un pointeur brut pour gérer votre indexation sur des centaines d'objets différents vaut la peine de perdre le contrôle de la mémoire.

Si la réponse est oui, utilisez un pointeur brut.

Si vous ne voulez même pas y réfléchir, une smart_ptrsolution est une bonne solution viable et géniale.

Claudiordgz
la source
4
ok, mais valgrind est bon pour vérifier d'éventuelles fuites de mémoire, donc tant que vous l'utilisez, vous devriez être en sécurité ™
graywolf
@Paladin Oui, si vous pouvez gérer votre mémoire, smart_ptrsont vraiment utiles pour les grandes équipes
Claudiordgz
3
J'utilise unique_ptr, cela simplifie beaucoup de choses, mais n'aime pas shared_ptr, le comptage de références n'est pas très efficace GC et ce n'est pas parfait non plus
graywolf
1
@Paladin J'essaie d'utiliser des pointeurs bruts si je peux tout encapsuler. Si c'est quelque chose que je vais passer partout comme un argument, alors peut-être que je considérerai un smart_ptr. La plupart de mes uniques_ptrs sont utilisés dans la grande implémentation, comme une méthode main ou run
Claudiordgz
@Lothar Je vois que vous avez paraphrasé l'une des choses que j'ai dites dans votre réponse: Thats why you should not do this unless the function is really involved in ownership management... excellente réponse, merci, voté pour
Claudiordgz
0

Juste pour un aperçu et juste pour l' []opérateur, il est ~ 5X plus lent que le pointeur brut, comme illustré dans le code suivant, qui a été compilé à l'aide de gcc -lstdc++ -std=c++14 -O0et a généré ce résultat:

malloc []:     414252610                                                 
unique []  is: 2062494135                                                
uq get []  is: 238801500                                                 
uq.get()[] is: 1505169542
new is:        241049490 

Je commence à apprendre le C ++, j'ai ceci en tête: il faut toujours savoir ce que tu fais et prendre plus de temps pour savoir ce que les autres ont fait dans ton C ++.

ÉDITER

Comme indiqué par @Mohan Kumar, j'ai fourni plus de détails. La version gcc est 7.4.0 (Ubuntu 7.4.0-1ubuntu1~14.04~ppa1), Le résultat ci-dessus a été obtenu lorsque le -O0est utilisé, cependant, lorsque j'utilise le drapeau '-O2', j'ai obtenu ceci:

malloc []:     223
unique []  is: 105586217
uq get []  is: 71129461
uq.get()[] is: 69246502
new is:        9683

Puis est passé à clang version 3.9.0, -O0était:

malloc []:     409765889
unique []  is: 1351714189
uq get []  is: 256090843
uq.get()[] is: 1026846852
new is:        255421307

-O2 était:

malloc []:     150
unique []  is: 124
uq get []  is: 83
uq.get()[] is: 83
new is:        54

Le résultat de clang -O2est incroyable.

#include <memory>
#include <iostream>
#include <chrono>
#include <thread>

uint32_t n = 100000000;
void t_m(void){
    auto a  = (char*) malloc(n*sizeof(char));
    for(uint32_t i=0; i<n; i++) a[i] = 'A';
}
void t_u(void){
    auto a = std::unique_ptr<char[]>(new char[n]);
    for(uint32_t i=0; i<n; i++) a[i] = 'A';
}

void t_u2(void){
    auto a = std::unique_ptr<char[]>(new char[n]);
    auto tmp = a.get();
    for(uint32_t i=0; i<n; i++) tmp[i] = 'A';
}
void t_u3(void){
    auto a = std::unique_ptr<char[]>(new char[n]);
    for(uint32_t i=0; i<n; i++) a.get()[i] = 'A';
}
void t_new(void){
    auto a = new char[n];
    for(uint32_t i=0; i<n; i++) a[i] = 'A';
}

int main(){
    auto start = std::chrono::high_resolution_clock::now();
    t_m();
    auto end1 = std::chrono::high_resolution_clock::now();
    t_u();
    auto end2 = std::chrono::high_resolution_clock::now();
    t_u2();
    auto end3 = std::chrono::high_resolution_clock::now();
    t_u3();
    auto end4 = std::chrono::high_resolution_clock::now();
    t_new();
    auto end5 = std::chrono::high_resolution_clock::now();
    std::cout << "malloc []:     " <<  (end1 - start).count() << std::endl;
    std::cout << "unique []  is: " << (end2 - end1).count() << std::endl;
    std::cout << "uq get []  is: " << (end3 - end2).count() << std::endl;
    std::cout << "uq.get()[] is: " << (end4 - end3).count() << std::endl;
    std::cout << "new is:        " << (end5 - end4).count() << std::endl;
}
liqg3
la source
J'ai testé le code maintenant, il n'est lent que de 10% lors de l'utilisation du pointeur unique.
Mohan Kumar
8
jamais de benchmark -O0ou de débogage de code. La sortie sera extrêmement inefficace . Toujours utiliser au moins -O2(ou de -O3nos jours car certaines vectorisations ne sont pas effectuées -O2)
phuclv
1
Si vous avez le temps et que vous voulez une pause-café, prenez -O4 pour optimiser le temps de liaison et toutes les petites fonctions d'abstraction sont intégrées et disparaissent.
Lothar
Vous devriez inclure un freeappel dans le test malloc, et delete[]pour new (ou make variable astatic), car les unique_ptrs appellent delete[]sous le capot, dans leurs destructeurs.
RnMss