c++ - weak_ptr - shared_ptr example
El costo de pasar por shared_ptr (5)
Uso std :: tr1 :: shared_ptr extensivamente en mi aplicación. Esto incluye pasar objetos como argumentos de funciones. Considera lo siguiente:
class Dataset {...}
void f( shared_ptr< Dataset const > pds ) {...}
void g( shared_ptr< Dataset const > pds ) {...}
...
Al pasar un objeto de conjunto de datos a través de shared_ptr garantiza su existencia dentro de f y g, las funciones se pueden llamar millones de veces, lo que hace que se creen y destruyan muchos objetos shared_ptr. Aquí hay un fragmento del perfil plano gprof de una ejecución reciente:
Each sample counts as 0.01 seconds. % cumulative self self total time seconds seconds calls s/call s/call name 9.74 295.39 35.12 2451177304 0.00 0.00 std::tr1::__shared_count::__shared_count(std::tr1::__shared_count const&) 8.03 324.34 28.95 2451252116 0.00 0.00 std::tr1::__shared_count::~__shared_count()
Por lo tanto, ~ 17% del tiempo de ejecución se gastó en el recuento de referencias con objetos shared_ptr. ¿Esto es normal?
Una gran parte de mi aplicación tiene un único subproceso y estaba pensando en volver a escribir algunas de las funciones como
void f( const Dataset& ds ) {...}
y reemplazando las llamadas
shared_ptr< Dataset > pds( new Dataset(...) );
f( pds );
con
f( *pds );
en lugares donde sé con certeza que el objeto no se destruirá mientras el flujo del programa esté dentro de f (). Pero antes de salir corriendo para cambiar un montón de firmas de función / llamadas, quería saber cuál era el golpe de rendimiento típico de pasar por shared_ptr. Parece que shared_ptr no se debe usar para las funciones a las que se llama con mucha frecuencia.
Cualquier entrada sería apreciada. Gracias por leer.
-Artem
Actualización: después de cambiar un puñado de funciones para aceptar const Dataset&
, el nuevo perfil se ve así:
Each sample counts as 0.01 seconds. % cumulative self self total time seconds seconds calls s/call s/call name 0.15 241.62 0.37 24981902 0.00 0.00 std::tr1::__shared_count::~__shared_count() 0.12 241.91 0.30 28342376 0.00 0.00 std::tr1::__shared_count::__shared_count(std::tr1::__shared_count const&)
Estoy un poco desconcertado por el número de llamadas al destructor que son más pequeñas que el número de llamadas al constructor de copia, pero en general estoy muy satisfecho con la disminución en el tiempo de ejecución asociado. Gracias a todos por su consejo.
Necesita shared_ptr solo para pasarlo a funciones / objetos que lo mantienen para uso futuro. Por ejemplo, algunas clases pueden mantener shared_ptr para usar en un hilo de trabajo. Para llamadas simples sincrónicas, es suficiente usar puntero simple o referencia. shared_ptr no debe reemplazar el uso de punteros simples por completo.
Parece que realmente sabes lo que estás haciendo. Ha perfilado su aplicación y sabe exactamente dónde se utilizan los ciclos. Comprende que llamar al constructor a un puntero de conteo de referencia es costoso solo si lo hace constantemente.
El único aviso que puedo darte es: supongamos que dentro de la función f (t * ptr), si llamas a otra función que usa punteros compartidos, y haces otro (ptr) y otro hace un puntero compartido del puntero sin procesar. Cuando el segundo recuento de referencias de los punteros compartidos llega a 0, entonces ha eliminado efectivamente su objeto ... aunque no haya querido hacerlo. dijiste que usaste punteros de recuento de referencia mucho, por lo que debes tener cuidado con casos de esquina como ese.
EDIT: puedes hacer que el destructor sea privado, y solo un amigo de la clase de puntero compartido, de modo que el destructor solo pueda ser llamado por un puntero compartido, entonces estás a salvo. No evita eliminaciones múltiples de punteros compartidos. Según el comentario de Mat.
Se debe evitar la creación y destrucción de objetos, especialmente la creación y destrucción de objetos redundantes, en aplicaciones de rendimiento crítico.
Considera lo que está haciendo shared_ptr. No solo crea un objeto nuevo y lo rellena, sino que también hace referencia al estado compartido para incrementar la información de referencia, y el objeto en sí mismo, presumiblemente, vive en otro lugar completamente, lo que será una pesadilla en su caché.
Presumiblemente necesita el shared_ptr (porque si pudiera salirse con la suya con un objeto local no asignaría uno del montón), pero incluso podría "almacenar en caché" el resultado de la desreferencia shared_ptr:
void fn(shared_ptr< Dataset > pds)
{
Dataset& ds = *pds;
for (i = 0; i < 1000; ++i)
{
f(ds);
g(ds);
}
}
... porque incluso * pds requiere golpear más memoria de la absolutamente necesaria.
Si no estás usando make_shared , ¿podrías dar una oportunidad? Al ubicar el recuento de referencias y el objeto en la misma área de memoria, puede ver una ganancia de rendimiento asociada con la coherencia de la memoria caché. Vale la pena intentarlo de todos modos.
Siempre pase su shared_ptr
por referencia de referencia:
void f(const shared_ptr<Dataset const>& pds) {...}
void g(const shared_ptr<Dataset const>& pds) {...}
Editar: Respecto a los problemas de seguridad mencionados por otros:
- Al usar
shared_ptr
gran medida a lo largo de una aplicación, pasar por valor tomará una gran cantidad de tiempo (lo he visto ir 50 +%). - Use
const T&
lugar deconst shared_ptr<T const>&
cuando el argumento no sea nulo. - Usar
const shared_ptr<T const>&
es más seguro queconst T*
cuando el rendimiento es un problema.