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

Resuelto sdgfsdh asked hace 7 años • 8 respuestas

Estuve revisando 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?

¿Tiene algún sentido 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;
}
sdgfsdh avatar Jan 26 '17 17:01 sdgfsdh
Aceptado

Creo que lo único que las otras respuestas no enfatizaron lo suficiente es el punto de 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, desperdiciando una tonelada de tiempo y recursos en el proceso.

Al mover el shared_ptren lugar de copiarlo, "robamos" el recuento de referencias atómicas y anulamos el otro shared_ptr. "robar" el recuento de referencias no es atómico y es cien veces más rápido que copiarlo shared_ptr(y provocar un incremento o disminución de la referencia atómica ).

Tenga en cuenta que esta técnica se utiliza únicamente para optimización. copiarlo (como sugirió) es igual de bueno en cuanto a funcionalidad.

David Haim avatar Jan 26 '2017 13:01 David Haim

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

Bo Persson avatar Jan 26 '2017 10:01 Bo Persson

Las operaciones de movimiento (como el constructor de movimientos) std::shared_ptrson económicas , ya que básicamente son "robo de punteros" (del origen al destino; para ser más precisos, todo el bloque de control de estado se "roba" del origen al destino, incluida la información del recuento de referencias). .

En su lugar, las operaciones de copia al std::shared_ptrinvocar un aumento del recuento de referencias atómicas (es decir, no solo ++RefCounten un RefCountmiembro de datos entero, sino, por ejemplo, InterlockedIncrementen Windows), lo cual es más costoso que simplemente robar punteros/estado.

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

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

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

  1. Al ingresar al método, el shared_ptrparámetro se copia y se construye: ref count incremento atómico .
  2. Dentro del cuerpo del método, copia el shared_ptrparámetro en el miembro de datos: ref count atomic increment .
  3. Al salir del método, el shared_ptrparámetro se destruye: ref count atomic decrement .

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 ingresar al método, el shared_ptrparámetro se copia y se construye: ref count incremento atómico .
  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ás robando punteros/estado: no están involucradas operaciones costosas de recuento de ref atómico.
  3. Al salir del método, el shared_ptrparámetro se destruye; pero desde que te mudaste en el paso 2, no hay nada que destruir, ya que el shared_ptrparámetro ya no apunta a nada. Nuevamente, en este caso no ocurre ningún decremento atómico.

En pocas palabras: en este caso obtienes solo un incremento atómico de recuento de referencias, 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.

Mr.C64 avatar Jan 26 '2017 11:01 Mr.C64

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 importante cuestión de mostrar la intención del código con mayor claridad.

Para un std::shared_ptr, std::move denota inequívocamente una transferencia de propiedad de la punta, 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, permitiendo que se destruya su std::shared_ptr), entonces se ha logrado una transferencia de propiedad.

Cuando transfieres la propiedad con std::move, es obvio lo que está sucediendo. Si utiliza una copia normal, no es obvio que la operación prevista sea una transferencia hasta que verifique que el propietario original renuncia 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 consiguientes cambios en el recuento de referencias).

Stephen C. Steel avatar Jan 26 '2017 18:01 Stephen C. Steel