¿Hacer compartido es realmente más eficiente que nuevo?


Estaba experimentando con shared_ptr y make_shared desde C++11 y programé un pequeño ejemplo de juguete para ver lo que realmente está sucediendo al llamar a make_shared. Como infraestructura estaba usando llvm / clang 3.0 junto con la biblioteca llvm std c++ dentro de 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;
}

Ahora echa un vistazo a la salida, por favor:

Cree smart_ptr usando make_shared...

Constructor make_shared

Constructor de copia...

Constructor de copia...

Destructor

Destructor

Cree smart_ptr usando make_shared: done.

Cree smart_ptr usando new...

Constructor nuevo

Cree smart_ptr usando new: done.

Destructor

Destructor

Parece que make_shared está llamando al constructor de copia dos veces. Si asigno memoria para un Object usando un new regular esto no sucede, solo uno Object es construir.

Lo que me pregunto es lo siguiente. He oído que make_shared se supone que es más eficiente que usar new(1, 2). Una razón es porque make_shared asigna el recuento de referencia junto con el objeto a administrar en el mismo bloque de memoria. Vale, ya entendí. Esto es, por supuesto, más eficiente que dos operaciones de asignación separadas.

Por el contrario, no entiendo por qué esto tiene que venir con el costo de dos llamadas al constructor de copia de Object. Debido a esto no estoy convencido de que make_shared sea más eficiente que la asignación usando new en cada caso. ¿Me equivoco? Bien, Uno podría implementar un constructor de movimiento para Object pero todavía no estoy seguro de si esto es más eficiente que simplemente asignar Object a través de new. Al menos no en todos los casos. Sería cierto si copiar Object es menos costoso que asignar memoria para un contador de referencia. Pero el shared_ptr-contador de referencia interno podría implementarse usando un par de tipos de datos primitivos, ¿verdad?

¿Puede ayudar y explicar por qué make_shared es el camino a seguir en términos de eficiencia, a pesar de la sobrecarga de copia delineada?

Author: Nic Hartley, 2012-02-16

4 answers

Como infraestructura estaba usando llvm/clang 3.0 junto con la biblioteca llvm std c++ dentro de XCode4.

Bueno, ese parece ser tu problema. El estándar C++11 establece los siguientes requisitos para make_shared<T> (y allocate_shared<T>), en la sección 20.7.2.2.6:

Requiere: La expresión:: new(pv) T(std::forward (args)...), cuando pv tenga el tipo void* y apunte a un almacenamiento adecuado para sostener un objeto de tipo T, deberá estar bien formado. A será un asignador (17.6.3.5). El constructor de copia y el destructor de A no lanzarán excepciones.

T es no necesario para ser la copia-construible. De hecho, T ni siquiera se requiere que sea construible sin colocación nueva. Solo se requiere que sea construible en el lugar. Esto significa que lo único que make_shared<T> puede hacer con T es new en su lugar.

Así que los resultados que obtienes no son consistentes con el estándar. LIBC++ de LLVM está roto en este sentido. Archivo de un error informe.

Para referencia, esto es lo que sucedió cuando llevé su código a 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

También lo porté a los originales de Boost shared_ptr y make_shared, y obtuve lo mismo que VC2010.

Sugeriría presentar un informe de error, ya que el comportamiento de libc++está roto.

 37
Author: Nicol Bolas,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-02-16 16:55:01

Tienes que comparar estas dos versiones:

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

En su código, la segunda variable es solo un puntero desnudo, no un puntero compartido en absoluto.


Ahora en la carne. make_shared es (en la práctica) más eficiente, porque asigna el bloque de control de referencia junto con el objeto real en una sola asignación dinámica. Por el contrario, el constructor de shared_ptr que toma un puntero de objeto desnudo debe asignar otra variable dinámica para la referencia contar. La compensación es que make_shared (o su primo allocate_shared) no le permite especificar un deleter personalizado, ya que la asignación es realizada por el asignador.

(Esto no afecta la construcción del objeto en sí. Desde la perspectiva de Object no hay diferencia entre las dos versiones. Lo que es más eficiente es el puntero compartido en sí, no el objeto administrado.)

 33
Author: Kerrek SB,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-02-15 22:31:25

Así que una cosa a tener en cuenta es su configuración de optimización. Medir el rendimiento, particularmente con respecto a c++ es sin sentido sin optimizaciones habilitadas. No se si de hecho compilaste con optimizaciones, así que pensé que valía la pena mencionarlo.

Dicho esto, lo que estás midiendo con esta prueba es no una manera que make_shared es más eficiente. En pocas palabras, usted está midiendo lo incorrecto: - P.

Este es el trato. Normalmente, cuando crear puntero compartido, tiene al menos 2 miembros de datos (posiblemente más). Uno para el puntero y otro para el recuento de referencia. Este recuento de referencia se asigna en el montón (para que pueda ser compartido entre shared_ptr con diferentes vidas...ese es el punto, después de todo!)

Así que si estás creando un objeto con algo como std::shared_ptr<Object> p2(new Object("foo")); Hay al menos 2 llamadas a new. Uno para Object y otro para el objeto de recuento de referencia.

make_shared tiene la opción (no estoy seguro de que tiene to), para hacer un único new que sea lo suficientemente grande como para contener el objeto apuntado y el recuento de referencia en el mismo bloque contiguo. Asignación efectiva de un objeto que se vea algo como esto (ilustrativo, no literalmente lo que es).

struct T {
    int reference_count;
    Object object;
};

Dado que el recuento de referencias y las vidas del objeto están unidas (no tiene sentido que uno viva más que el otro). Este bloque entero puede ser deleted al mismo tiempo también.

Así que la eficiencia está en las asignaciones, no en la copia (que sospecho que tenía que ver con la optimización más que cualquier otra cosa).

Para ser claros, esto es lo que boost tiene que decir sobre make_shared

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

Además de la comodidad y el estilo, tal función también es segura para excepciones y considerablemente más rápido, ya que puede utilizar una sola asignación para tanto el objeto como su correspondiente bloque de control, eliminando un significativo parte de la sobrecarga de construcción de shared_ptr. Este elimina una de las principales quejas de eficiencia sobre shared_ptr.

 6
Author: Evan Teran,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-02-04 22:29:39

No debería recibir ninguna copia extra allí. La salida debe ser:

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

No se porque estas recibiendo copias extra. (aunque veo que está recibiendo un 'Destructor' demasiados, por lo que el código que utilizó para obtener su salida debe ser diferente del código que publicó)

make_shared es más eficiente porque se puede implementar usando solo una asignación dinámica en lugar de dos, y porque necesita el valor de un puntero de memoria menos mantenimiento de libros por compartido objeto.

Editar: No comprobé con Xcode 4.2 pero con Xcode 4.3 obtengo la salida correcta que muestro arriba, no la salida incorrecta que se muestra en la pregunta.

 3
Author: bames53,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-02-17 05:47:05