c++ - ¿Es make_shared realmente más eficiente que nuevo?
shared-ptr clang (4)
Estaba experimentando con shared_ptr
y make_shared
desde C ++ 11 y make_shared
un pequeño ejemplo de juguete para ver qué está sucediendo realmente cuando llamo 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 eche un vistazo a la salida, por favor:
Crea smart_ptr usando make_shared ...
Constructor make_shared
Copiar constructor ...
Copiar constructor ...
Incinerador de basuras
Incinerador de basuras
Crear smart_ptr usando make_shared: hecho.
Crear smart_ptr usando nuevo ...
Constructor nuevo
Crear smart_ptr usando new: hecho.
Incinerador de basuras
Incinerador de basuras
Parece que make_shared
llama al constructor de copia dos veces. Si asigna memoria para un Object
usando un new
estándar esto no ocurre, solo se construye un Object
.
Lo que me pregunto es lo siguiente. Escuché 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 referencias junto con el objeto que se gestionará en el mismo bloque de memoria. OK, entendí el punto. 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 con new
en todos los casos. ¿Me equivoco aquí? 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 new
. Al menos no en todos los casos. Sería cierto si la copia de Object
es menos costosa que la asignación de memoria para un contador de referencia. Pero el shared_ptr
referencia shared_ptr
-internal podría implementarse utilizando un par de tipos de datos primitivos, ¿verdad?
¿Puedes ayudar y explicar por qué make_shared
es el camino a seguir en términos de eficiencia, a pesar de la sobrecarga de la copia delineada?
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) ...), donde pv tiene tipo void * y apunta a un almacenamiento adecuado para contener un objeto de tipo T, estará bien formada. A debe ser un asignador (17.6.3.5). El constructor de copia y el destructor de A no lanzarán excepciones.
No se requiere que T
sea copiable. De hecho, T
ni siquiera se requiere que sea constructable sin ubicació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.
Entonces, los resultados que obtiene no son consistentes con el estándar. La biblioteca de LLVM está rota en este sentido. Presente un informe de error.
Como referencia, esto es lo que sucedió cuando tomé su código en 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 shared_ptr
original shared_ptr
y make_shared
, y obtuve lo mismo que VC2010.
Sugeriría que se presente un informe de error, ya que el comportamiento de libc ++ está roto.
No deberías obtener ninguna copia adicional allí. El resultado debería 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 sé por qué estás recibiendo copias adicionales. (Aunque veo que está obteniendo demasiados ''Destructor'', por lo que el código que usó para obtener su salida debe ser diferente del código que publicó)
make_shared
es más eficiente porque se puede implementar utilizando solo una asignación dinámica en lugar de dos, y porque necesita una memoria de un puntero menos contabilidad por objeto compartido.
Editar: No revisé 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.
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, ya que asigna el bloque de control de referencia junto con el objeto real en una única asignación dinámica. Por el contrario, el constructor para shared_ptr
que toma un puntero de objeto desnudo debe asignar otra variable dinámica para el recuento de referencia. El trade-off es que make_shared
(o su primo allocate_shared
) no le permite especificar un eliminador personalizado, ya que la asignación la realiza 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í mismo, no el objeto gestionado).
Una cosa a tener en cuenta es la configuración de optimización. Medir el rendimiento, particularmente con respecto a c ++ no tiene sentido sin optimizaciones habilitadas. No sé si de hecho recopiló optimizaciones, así que pensé que valía la pena mencionarlo.
Dicho eso, lo que está midiendo con esta prueba no es una forma en que make_shared
sea más eficiente. En pocas palabras, estás midiendo lo incorrecto :-P.
Aquí está el trato. Normalmente, cuando creas 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 compartirse entre shared_ptr
con diferentes shared_ptr
vida ... ¡ese es el punto después de todo!)
Entonces, si está 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 uno para el objeto de conteo de referencia.
make_shared
tiene la opción (no estoy seguro de que tenga que hacerlo) para hacer una sola new
que sea lo suficientemente grande como para mantener el objeto apuntado y el recuento de referencias en el mismo bloque contiguo. Asignación efectiva de un objeto que se parece a 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 tiempo que el otro). Este bloque completo puede delete
al mismo tiempo también.
Entonces, la eficiencia está en las asignaciones, no en las copias (lo que sospecho tiene que ver con la optimización más que con cualquier otra cosa).
Para que quede claro, esto es lo que tiene que decir make_shared
sobre make_shared
http://www.boost.org/doc/libs/1_43_0/libs/smart_ptr/make_shared.html
Además de la conveniencia y el estilo, dicha función también es excepcionalmente segura y considerablemente más rápida porque puede usar una única asignación tanto para el objeto como para su bloque de control correspondiente, eliminando una porción significativa de la sobrecarga de construcción de shared_ptr. Esto elimina una de las principales quejas de eficiencia sobre shared_ptr.