¿Por qué debería std :: mover un std :: shared_ptr?


148

He estado buscando el código fuente de Clang y encontré este fragmento:

void CompilerInstance::setInvocation(
    std::shared_ptr<CompilerInvocation> Value) {
  Invocation = std::move(Value);
}

¿Por qué querría std::moveun std::shared_ptr?

¿Hay algún punto para transferir la propiedad de un recurso compartido?

¿Por qué no haría esto en su lugar?

void CompilerInstance::setInvocation(
    std::shared_ptr<CompilerInvocation> Value) {
  Invocation = Value;
}

Respuestas:


137

Creo que lo único que las otras respuestas no enfatizaron lo suficiente es el punto de la velocidad .

std::shared_ptrEl recuento de referencia es atómico . aumentar o disminuir el recuento de referencia requiere un incremento o decremento atómico . Esto es cien veces más lento que el incremento / decremento no atómico , sin mencionar que si incrementamos y disminuimos el mismo contador, terminamos con el número exacto, perdiendo una tonelada de tiempo y recursos en el proceso.

Al mover el en shared_ptrlugar de copiarlo, "robamos" el recuento de referencia atómica y anulamos el otro shared_ptr. "robar" el recuento de referencia no es atómico , y es cien veces más rápido que copiarlo shared_ptr(y causar un aumento o disminución de la referencia atómica ).

Tenga en cuenta que esta técnica se utiliza exclusivamente para la optimización. copiarlo (como sugirió) es tan bueno en cuanto a funcionalidad.


55
¿Es realmente cien veces más rápido? ¿Tienes puntos de referencia para esto?
xaviersjs

1
@xaviersjs La asignación requiere un incremento atómico seguido de una disminución atómica cuando el valor se sale del alcance. Las operaciones atómicas pueden tomar cientos de ciclos de reloj. Entonces sí, es mucho más lento.
Adisak

2
@Adisak es la primera vez que escucho la operación de búsqueda y adición ( en.wikipedia.org/wiki/Fetch-and-add ) podría tomar cientos de ciclos más que un incremento básico. Tienes una referencia para eso?
xaviersjs

2
@xaviersjs: stackoverflow.com/a/16132551/4238087 Con operaciones de registro de unos pocos ciclos, 100's (100-300) de ciclos para atómica se ajustan perfectamente. Aunque las métricas son de 2013, esto todavía parece ser cierto, especialmente para los sistemas NUMA multi-socket.
russianfool

1
A veces piensas que no hay hilos en tu código ... pero luego aparece una maldita biblioteca que te arruina. Es mejor usar referencias constantes y std :: move ... si es claro y obvio que puede ... que confiar en los recuentos de referencias de puntero.
Erik Aronesty

123

Al usarlo move, evita aumentar y luego disminuir inmediatamente el número de acciones. Eso podría ahorrarle algunas operaciones atómicas costosas en el recuento de uso.


1
¿No es una optimización prematura?
YSC

11
@YSC no si quien lo puso allí realmente lo probó.
OrangeDog

19
La optimización prematura de @YSC es mala si hace que el código sea más difícil de leer o mantener. Este tampoco, al menos IMO.
Angew ya no está orgulloso de SO

17
En efecto. Esta no es una optimización prematura. Es, en cambio, la forma sensata de escribir esta función.
ligereza corre en órbita el

60

Las operaciones de movimiento (como el constructor de movimientos) std::shared_ptrson baratas , ya que básicamente son "robo de punteros" (de origen a destino; para ser más precisos, todo el bloque de control de estado es "robado" de origen a destino, incluida la información de recuento de referencia) .

En cambio, las operaciones de copia en std::shared_ptrinvocación aumentan el recuento de referencia atómica (es decir, no solo ++RefCounten un RefCountmiembro de datos enteros , sino, por ejemplo, InterlockedIncrementen Windows), que es más costoso que simplemente robar punteros / estado.

Entonces, analizando la dinámica de recuento de ref de este caso en detalle:

// shared_ptr<CompilerInvocation> sp;
compilerInstance.setInvocation(sp);

Si pasa sppor valor y luego toma una copia dentro del CompilerInstance::setInvocationmétodo, tiene:

  1. Al entrar en el método, los shared_ptres un parámetro copia construidos: ref contar atómica de la subasta .
  2. Dentro del cuerpo del método, copie el shared_ptrparámetro en el miembro de datos: incremento atómico de recuento de referencia .
  3. Al salir del método, el shared_ptrparámetro se destruye: decremento atómico de recuento de ref .

Tiene dos incrementos atómicos y un decremento atómico, para un total de tres operaciones atómicas .

En cambio, si pasa el shared_ptrparámetro por valor y luego std::movedentro del método (como se hace correctamente en el código de Clang), tiene:

  1. Al entrar en el método, los shared_ptres un parámetro copia construidos: ref contar atómica de la subasta .
  2. Dentro del cuerpo del método, ingresa std::moveel shared_ptrparámetro en el miembro de datos: ¡el recuento de referencias no cambia! Solo está robando punteros / estado: no hay operaciones costosas de conteo de ref.
  3. Al salir del método, el shared_ptrparámetro se destruye; pero como te moviste en el paso 2, no hay nada que destruir, ya que el shared_ptrparámetro ya no apunta a nada. Una vez más, no se produce una disminución atómica en este caso.

En pocas palabras: en este caso, obtienes solo un incremento atómico de recuento de referencia, es decir, solo una operación atómica .
Como puede ver, esto es mucho mejor que dos incrementos atómicos más un decremento atómico (para un total de tres operaciones atómicas) para el caso de copia.


1
También vale la pena señalar: ¿por qué no pasan simplemente por referencia constante y evitan todo el material std :: move? Porque el paso por valor también le permite pasar un puntero sin procesar directamente y solo se creará un shared_ptr.
Joseph Ireland

@JosephIreland Porque no puedes mover una referencia constante
Bruno Ferreira

2
@JosephIreland porque si lo llamas así, compilerInstance.setInvocation(std::move(sp));no habrá incremento . Puede obtener el mismo comportamiento agregando una sobrecarga que requiere un shared_ptr<>&&pero por qué duplicar cuando no es necesario.
Ratchet freak

2
@BrunoFerreira Estaba respondiendo mi propia pregunta. No necesitaría moverlo porque es una referencia, solo cópielo. Todavía solo una copia en lugar de dos. La razón por la que no lo hacen es porque sería innecesariamente copiar shared_ptrs de nueva construcción, por ejemplo de setInvocation(new CompilerInvocation), o como se mencionó trinquete, setInvocation(std::move(sp)). Lo siento si mi primer comentario no fue claro, en realidad lo publiqué por accidente, antes de terminar de escribir, y decidí dejarlo
Joseph Ireland

22

Copiar un shared_ptrimplica copiar su puntero de objeto de estado interno y cambiar el recuento de referencia. Moverlo solo implica intercambiar punteros al contador de referencia interno y al objeto propio, por lo que es más rápido.


16

Hay dos razones para usar std :: move en esta situación. La mayoría de las respuestas abordaron la cuestión de la velocidad, pero ignoraron la cuestión importante de mostrar la intención del código más claramente.

Para un std :: shared_ptr, std :: move denota inequívocamente una transferencia de propiedad del puntero, mientras que una simple operación de copia agrega un propietario adicional. Por supuesto, si el propietario original posteriormente renuncia a su propiedad (por ejemplo, al permitir que se destruya su std :: shared_ptr), se ha realizado una transferencia de propiedad.

Cuando transfiere la propiedad con std :: move, es obvio lo que está sucediendo. Si usa una copia normal, no es obvio que la operación prevista es una transferencia hasta que verifique que el propietario original renuncie inmediatamente a la propiedad. Como beneficio adicional, es posible una implementación más eficiente, ya que una transferencia atómica de propiedad puede evitar el estado temporal en el que el número de propietarios ha aumentado en uno (y los cambios correspondientes en los recuentos de referencia).


Exactamente lo que estoy buscando. Me sorprende cómo otras respuestas ignoran esta importante diferencia semántica. Los punteros inteligentes tienen que ver con la propiedad.
qweruiop

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.