web-dev-qa-db-fra.com

Make_shared est-il vraiment plus efficace que nouveau?

J'expérimentais avec shared_ptr et make_shared à partir de C++ 11 et a programmé un petit exemple de jouet pour voir ce qui se passe réellement lors de l'appel de make_shared. En tant qu'infrastructure, j'utilisais llvm/clang 3.0 avec la bibliothèque llvm std c ++ dans XCode4.

class Object
{
public:
    Object(const string& str)
    {
        cout << "Constructor " << str << endl;
    }

    Object()
    {
        cout << "Default constructor" << endl;

    }

    ~Object()
    {
        cout << "Destructor" << endl;
    }

    Object(const Object& rhs)
    {
        cout << "Copy constructor..." << endl;
    }
};

void make_shared_example()
{
    cout << "Create smart_ptr using make_shared..." << endl;
    auto ptr_res1 = make_shared<Object>("make_shared");
    cout << "Create smart_ptr using make_shared: done." << endl;

    cout << "Create smart_ptr using new..." << endl;
    shared_ptr<Object> ptr_res2(new Object("new"));
    cout << "Create smart_ptr using new: done." << endl;
}

Jetez maintenant un œil à la sortie, veuillez:

Créer smart_ptr en utilisant make_shared ...

Constructeur make_shared

Copier le constructeur ...

Copier le constructeur ...

Destructeur

Destructeur

Créez smart_ptr en utilisant make_shared: done.

Créer smart_ptr en utilisant nouveau ...

Constructeur nouveau

Créez smart_ptr en utilisant new: done.

Destructeur

Destructeur

Il semble que make_shared appelle deux fois le constructeur de copie. Si j'alloue de la mémoire pour un Object en utilisant un new normal, cela ne se produit pas, un seul Object est construit.

Ce que je me demande, c'est ce qui suit. J'ai entendu cela make_shared est censé être plus efficace que l'utilisation de new( 1 , 2 ). L'une des raisons est que make_shared alloue le compte de référence avec l'objet à gérer dans le même bloc de mémoire. OK, j'ai compris. Ceci est bien sûr plus efficace que deux opérations d'allocation distinctes.

Au contraire, je ne comprends pas pourquoi cela doit s'accompagner du coût de deux appels au constructeur de copie de Object. Pour cette raison, je ne suis pas convaincu que make_shared est plus efficace que l'allocation en utilisant new dans tous les cas. Ai-je tort ici? Eh bien, on pourrait implémenter un constructeur de déplacement pour Object mais je ne sais toujours pas si cela est plus efficace que simplement allouer Object à new. Du moins pas dans tous les cas. Ce serait vrai si la copie de Object est moins coûteuse que l'allocation de mémoire pour un compteur de référence. Mais le shared_ptr- le compteur de référence interne pourrait être implémenté en utilisant quelques types de données primitifs, non?

Pouvez-vous aider et expliquer pourquoi make_shared est la voie à suivre en termes d'efficacité, malgré la surcharge de copie décrite?

50
user1212354

En tant qu'infrastructure, j'utilisais llvm/clang 3.0 avec la bibliothèque llvm std c ++ dans XCode4.

Eh bien, cela semble être votre problème. La norme C++ 11 énonce les exigences suivantes pour make_shared<T> (et allocate_shared<T>), à la section 20.7.2.2.6:

Requiert: L'expression :: new (pv) T (std :: forward (args) ...), où pv a le type void * et pointe vers le stockage approprié pour contenir un objet de type T, doit être bien formée. A doit être un allocateur (17.6.3.5). Le constructeur de copie et le destructeur de A ne lèveront pas d'exceptions.

T est pas doit être constructible par copie. En effet, T n'a même pas besoin d'être constructible sans placement. Il est seulement nécessaire d'être constructible sur place. Cela signifie que la seule chose que make_shared<T> peut faire avec T est new il en place.

Les résultats que vous obtenez ne sont donc pas conformes à la norme. La libc ++ de LLVM est cassée à cet égard. Déposer un rapport de bogue.

Pour référence, voici ce qui s'est passé lorsque j'ai pris votre code dans VC2010:

Create smart_ptr using make_shared...
Constructor make_shared
Create smart_ptr using make_shared: done.
Create smart_ptr using new...
Constructor new
Create smart_ptr using new: done.
Destructor
Destructor

Je l'ai également porté sur l'original de Boost shared_ptr et make_shared, et j'ai eu la même chose que VC2010.

Je suggère de déposer un rapport de bogue, car le comportement de libc ++ est cassé.

37
Nicol Bolas

Vous devez comparer ces deux versions:

std::shared_ptr<Object> p1 = std::make_shared<Object>("foo");
std::shared_ptr<Object> p2(new Object("foo"));

Dans votre code, la deuxième variable est juste un pointeur nu, pas du tout un pointeur partagé.


Maintenant sur la viande. make_sharedest (en pratique) plus efficace, car il alloue le bloc de contrôle de référence avec l'objet réel en une seule allocation dynamique. En revanche, le constructeur de shared_ptr qui prend un pointeur d'objet nu doit allouer une autre variable dynamique pour le compte de référence. Le compromis est que make_shared (ou son cousin allocate_shared) ne vous permet pas de spécifier un suppresseur personnalisé, car l'allocation est effectuée par l'allocateur.

(Cela n'affecte pas la construction de l'objet lui-même. Du point de vue de Object, il n'y a pas de différence entre les deux versions. Ce qui est plus efficace est le pointeur partagé lui-même, pas l'objet géré.)

32
Kerrek SB

Donc, une chose à garder à l'esprit est vos paramètres d'optimisation. La mesure des performances, en particulier en ce qui concerne c ++, n'a aucun sens sans optimisation activée. Je ne sais pas si vous avez effectivement compilé avec des optimisations, j'ai donc pensé que cela valait la peine d'être mentionné.

Cela dit, ce que vous mesurez avec ce test n'est pas une manière dont make_shared Est plus efficace. Autrement dit, vous mesurez la mauvaise chose :-P.

Voici l'affaire. Normalement, lorsque vous créez un pointeur partagé, il a au moins 2 membres de données (éventuellement plus). Un pour le pointeur et un pour le nombre de références. Ce nombre de références est alloué sur le tas (afin qu'il puisse être partagé entre shared_ptr Avec différentes durées de vie ... c'est le point après tout!)

Donc, si vous créez un objet avec quelque chose comme std::shared_ptr<Object> p2(new Object("foo")); Il y a au moins 2 appels à new. Un pour Object et un pour l'objet de comptage de référence.

make_shared A la possibilité (je n'en suis pas sûr) de faire un seul new qui soit assez grand pour contenir l'objet pointé et le nombre de références dans le même bloc contigu. Allouer efficacement un objet qui ressemble à quelque chose comme ça (illustratif, pas littéralement ce que c'est).

struct T {
    int reference_count;
    Object object;
};

Étant donné que le nombre de références et les durées de vie de l'objet sont liés (il n'est pas logique que l'un vive plus longtemps que l'autre). Ce bloc entier peut également être deleted en même temps.

L'efficacité réside donc dans les allocations, pas dans la copie (ce qui, je le soupçonne, avait plus à voir avec l'optimisation qu'avec n'importe quoi d'autre).

Pour être clair, c'est ce que boost a à dire sur make_shared

http://www.boost.org/doc/libs/1_43_0/libs/smart_ptr/make_shared.html

Outre la commodité et le style, une telle fonction est également exceptionnellement sûre et considérablement plus rapide car elle peut utiliser une allocation unique pour l'objet et son bloc de contrôle correspondant, éliminant une partie importante de la surcharge de construction de shared_ptr. Cela élimine l'une des principales plaintes d'efficacité concernant shared_ptr.

6
Evan Teran

Vous ne devriez pas en obtenir de copies supplémentaires. La sortie doit être:

Create smart_ptr using make_shared...
Constructor make_shared
Create smart_ptr using make_shared: done.
Create smart_ptr using new...
Constructor new
Create smart_ptr using new: done.
Destructor

Je ne sais pas pourquoi vous obtenez des copies supplémentaires. (même si je vois que vous obtenez un 'Destructor' de trop, donc le code que vous avez utilisé pour obtenir votre sortie doit être différent du code que vous avez publié)

make_shared est plus efficace car il peut être implémenté en utilisant une seule allocation dynamique au lieu de deux, et parce qu'il a besoin de la valeur d'un pointeur de mémoire moins la tenue de livres par objet partagé.

Edit: je n'ai pas vérifié avec Xcode 4.2 mais avec Xcode 4.3 j'obtiens la sortie correcte que je montre ci-dessus, pas la sortie incorrecte montrée dans la question.

3
bames53