web-dev-qa-db-fra.com

Quel est le surcoût des pointeurs intelligents par rapport aux pointeurs normaux en C ++?

Quel est le surcoût 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, dans l'affirmative, de combien de temps?

Plus précisément, je vous parle du C++ 11 std::shared_ptr et std::unique_ptr.

De toute évidence, le contenu de la pile va être plus gros (du moins, je le crois), car un pointeur intelligent doit également stocker son état interne (nombre de références, etc.), la question est vraiment de savoir combien est-ce que cela va affecter ma performance, le cas échéant?

Par exemple, je retourne 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 en tant que paramètre au lieu d'un pointeur normal:

void setValue(std::shared_ptr<const Value> val);
// versus
void setValue(const Value *val);
82
Venemo

std::unique_ptr n'a de mémoire en surcharge que si vous lui fournissez un deleter non trivial.

std::shared_ptr a toujours une surcharge de mémoire pour le compteur de références, même s’il est très petit.

std::unique_ptr _ ne dispose de temps supplémentaire que pendant le constructeur (s'il doit copier le deleter fourni et/ou null-initialiser le pointeur) et pendant le destructeur (pour détruire l'objet possédé).

std::shared_ptr a 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é du fil de std::shared_ptr, ces incréments/décréments sont atomiques, ce qui ajoute un peu plus de temps système.

Notez qu’aucun d’entre eux n’a une surcharge de temps en déréférencement (pour obtenir la référence à un objet en propriété), 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 à moins de créer et de détruire en permanence des pointeurs intelligents.

158
lisyarus

Comme pour toute performance de code, le seul moyen réellement fiable d'obtenir des informations précises est de mesurer et/ou inspecter code machine.

Cela dit, un raisonnement simple dit que

  • Vous pouvez vous attendre à une surcharge dans les versions de débogage, car par exemple operator-> doit être exécuté en tant qu'appel de fonction pour que vous puissiez y accéder (ceci est dû au manque général de prise en charge du marquage des classes et des fonctions en tant que non-débogage).

  • Pour shared_ptr vous pouvez vous attendre à une surcharge lors de la création initiale, car cela implique l’allocation dynamique d’un bloc de contrôle et que l’allocation dynamique est beaucoup plus lente que toute autre opération de base en C++ (utilisez make_shared lorsque cela est pratiquement possible, afin de minimiser ces frais généraux).

  • Aussi pour shared_ptr il existe un surcoût minimal lié au maintien d’un nombre de références, par ex. en passant un shared_ptr par valeur, mais il n'y a pas de telle surcharge pour unique_ptr.

En gardant à l'esprit le premier point ci-dessus, lorsque vous effectuez une mesure, faites-le à la fois pour les versions de débogage et de libération.

Le comité international de normalisation C++ a publié un rapport technique sur les performances , mais c'était en 2006, avant le unique_ptr et shared_ptr ont été ajoutés à la bibliothèque standard. Néanmoins, les pointeurs intelligents étaient vieux jeu à ce moment-là, le rapport a également tenu compte de cela. Citant la partie pertinente:

"Si l'accès à une valeur via un pointeur intelligent trivial est nettement plus lent que si vous y accédez via un pointeur ordinaire, le compilateur gère de manière inefficace l'abstraction. Dans le passé, la plupart des compilateurs imposaient des pénalités d’abstraction importantes et plusieurs compilateurs actuels l’avaient encore. Cependant, au moins deux compilateurs auraient des pénalités d'abstraction inférieures à 1% et un autre une pénalité de 3%, de sorte que l'élimination de ce type de frais généraux est tout à fait dans les règles de l'art "

Comme une conjecture informé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.

23

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

shared_ptr a un temps système important 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 sur toutes les références faibles). Cela entraîne également une surcharge de mémoire et le fait que std :: shared_ptr est toujours un pointeur Tuple à 2 points (un pour l'objet, un pour 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 de nombreux codes dans le segment de code pour le déroulement de la pile. Si vous le transmettez par référence, vous obtenez un indirection supplémentaire, ce 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 vous assurer que votre objet n'est pas tué lors d'un appel de fonction normal.

Si vous devenez fou et que vous utilisez shared_ptr sur de petits objets tels qu'un arbre de syntaxe abstraite dans un compilateur ou sur de petits nœuds dans toute autre structure de graphe, vous constaterez une perte de performances énorme et une augmentation considérable de la mémoire. J'ai vu un système d'analyse syntaxique qui a été réécrit peu après que C++ 14 soit arrivé sur le marché et avant que le programmeur ait appris à 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 indicateurs bruts ne sont pas mauvais non plus par définition. Les mauvais programmeurs sont mauvais et le mauvais design est mauvais. Concevez avec soin, concevez avec une propriété claire à l'esprit et essayez d'utiliser le shared_ptr principalement sur la limite d'API de sous-système.

Si vous voulez en savoir plus, regardez Nicolai M. Josuttis qui parle bien du "Prix réel des pointeurs partagés en C++" https://vimeo.com/131189627
Il approfondit les détails de la mise en œuvre et l'architecture du processeur pour les barrières en écriture, les verrous atomiques, etc. Si vous voulez juste une preuve de la magnitude plus lente, sautez les 48 premières minutes et regardez-le lancer un exemple de code qui s'exécute jusqu'à 180 fois plus lentement (compilé avec -O3) lorsqu'il utilise le pointeur partagé partout.

18
Lothar

En d'autres termes, mon code va-t-il être plus lent si j'utilise des pointeurs intelligents et, dans l'affirmative, de combien de temps?

Ralentissez? Probablement pas, à moins que vous ne créiez un index énorme en utilisant shared_ptrs et que vous ne disposiez pas de suffisamment de mémoire au point que votre ordinateur commence à se froisser, comme une vieille dame qui tombe à terre par une force insupportable de loin.

Ce qui ralentirait votre code, ce sont les recherches lentes, le traitement inutile des boucles, les copies de données volumineuses et de nombreuses opérations d'écriture sur le disque (par exemple, 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 parcourez un tableau de 3 phases, chaque phase ayant un tableau de 1024 éléments. Créer un smart_ptr pour ce processus peut être excessif, car une fois l'itération terminée, vous saurez que vous devez l'effacer. Donc, vous pourriez gagner de la mémoire supplémentaire en n'utilisant pas un smart_ptr...

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

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

En fin de compte, il s’agit vraiment de… pouvez-vous gérer ce risque? Il est préférable de perdre le contrôle de la mémoire en utilisant un pointeur brut pour gérer votre indexation sur des centaines d'objets différents.

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

Si vous ne voulez même pas y penser, un smart_ptr est une bonne solution viable et géniale.

12
Claudiordgz

Juste pour avoir un aperçu et pour l'opérateur [], Il est environ 5 fois plus lent que le pointeur brut, comme le montre le code suivant, qui a été compilé avec gcc -lstdc++ -std=c++14 -O0 Et a généré le résultat suivant:

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

Je commence à apprendre le c ++, cela m’a l’esprit: vous devez toujours savoir ce que vous faites et prendre plus de temps pour savoir ce que les autres avaient fait dans votre c ++.

MODIFIER

Comme l'a dit @ Mohan Kumar, j'ai fourni plus de détails. La version de gcc est 7.4.0 (Ubuntu 7.4.0-1ubuntu1~14.04~ppa1), le résultat ci-dessus a été obtenu lorsque le paramètre -O0 Est utilisé. Toutefois, lorsque j'ai utilisé l'indicateur '-O2', j'ai obtenu ceci:

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

Puis déplacé vers 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 -O2 Est 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;
}

1
liqg3