c++ - smart - ¿Por qué debería std:: mover un std:: shared_ptr?
unique pointer (5)
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::move
un
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;
}
Al usar
move
evitas aumentar y luego disminuir inmediatamente el número de acciones.
Eso podría ahorrarle algunas operaciones atómicas costosas en el recuento de uso.
Copiar un
shared_ptr
implica 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.
Creo que lo único que las otras respuestas no enfatizaron lo suficiente es el punto de la velocidad .
std::shared_ptr
referencia
std::shared_ptr
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
shared_ptr
lugar de copiarlo, "robamos" el recuento de referencia
atómica
y
shared_ptr
el otro
shared_ptr
.
"robar" el recuento de referencia no es
atómico
, y es cien veces más rápido que copiar
shared_ptr
(y causar un incremento o decremento de 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.
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 operación de copia simple 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).
Las
operaciones de
movimiento
(como move constructor) para
std::shared_ptr
son
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, incluido el información del recuento de referencia).
En cambio, las operaciones de
copia
en
std::shared_ptr
invocan
el
aumento del recuento de referencias
atómicas
(es decir, no solo
++RefCount
en un miembro de datos entero
RefCount
, sino, por ejemplo, llamar a
InterlockedIncrement
en 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
sp
por valor y luego toma una
copia
dentro del método
CompilerInstance::setInvocation
, tiene:
-
Al ingresar al método, el parámetro
shared_ptr
se construye con copia: ref count atomic increment . -
Dentro del cuerpo del método,
copie
el parámetro
shared_ptr
en el miembro de datos: incremento atómico de recuento de referencia. -
Al salir del método, el parámetro
shared_ptr
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 parámetro
shared_ptr
por valor y luego
std::move
dentro del método (como se hace correctamente en el código de Clang), tiene:
-
Al ingresar al método, el parámetro
shared_ptr
se construye con copia: ref count atomic increment . -
Dentro del cuerpo del método,
std::move
el parámetroshared_ptr
al miembro de datos: ¡el recuento de referencias no cambia! Solo está robando punteros / estado: no hay operaciones costosas de conteo de ref. Atómica. -
Al salir del método, el parámetro
shared_ptr
se destruye; pero como teshared_ptr
en el paso 2, no hay nada que destruir, ya que el parámetroshared_ptr
ya no apunta a nada. Nuevamente, no ocurre decremento atómico 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.