Por que eu std :: move um std :: shared_ptr?

147

Estive pesquisando o código-fonte do Clang e encontrei este trecho:

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

Por que eu iria querer std::moveum std::shared_ptr?

Existe algum ponto em transferir a propriedade de um recurso compartilhado?

Por que eu não faria isso?

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

Respostas:

136

Eu acho que a única coisa que as outras respostas não enfatizaram o suficiente é o ponto da velocidade .

std::shared_ptra contagem de referência é atômica . aumentar ou diminuir a contagem de referência requer incremento ou decremento atômico . Isso é cem vezes mais lento que o incremento / decremento não atômico , sem mencionar que, se incrementarmos e diminuirmos o mesmo contador, acabaremos com o número exato, desperdiçando uma tonelada de tempo e recursos no processo.

Movendo o em shared_ptrvez de copiá-lo, "roubamos" a contagem de referência atômica e anulamos o outro shared_ptr. "roubar" a contagem de referência não é atômica e é cem vezes mais rápido que copiar o shared_ptr(e causar incremento ou decréscimo de referência atômica ).

Observe que essa técnica é usada exclusivamente para otimização. copiá-lo (como você sugeriu) é tão bom quanto em termos de funcionalidade.

David Haim
fonte
5
É realmente cem vezes mais rápido? Você tem referências para isso?
Xaviersjs
1
@xaviersjs A atribuição requer um incremento atômico seguido de um decréscimo atômico quando Value fica fora do escopo. As operações atômicas podem levar centenas de ciclos de relógio. Então, sim, é realmente muito mais lento.
Adisak
2
@ Adisak, a primeira vez que ouvi a operação de busca e adição ( pt.wikipedia.org/wiki/Fetch-and-add ) pode levar centenas de ciclos a mais do que um incremento básico. Você tem uma referência para isso?
xaviersjs
2
@xaviersjs: stackoverflow.com/a/16132551/4238087 Com as operações de registro sendo de alguns ciclos, 100 (100-300) de ciclos para atômica são adequados. Embora as métricas sejam de 2013, isso ainda parece verdadeiro, especialmente para sistemas NUMA com vários soquetes.
russianfool
1
Às vezes, você acha que não há encadeamento no seu código ... mas, em seguida, alguma biblioteca danada aparece e a destrói para você. É melhor usar referências const e std :: move ... se é claro e óbvio que você pode .... do que confiar nas contagens de referências de ponteiros.
Erik Aronesty
122

Ao usar, movevocê evita aumentar e diminuir imediatamente o número de compartilhamentos. Isso pode economizar algumas operações atômicas caras na contagem de uso.

Bo Persson
fonte
1
Não é otimização prematura?
YSC 27/01
11
@YSC não se quem o colocou lá realmente o testou.
precisa saber é o seguinte
19
@YSC A otimização prematura é ruim se dificulta a leitura ou a manutenção do código. Este não faz, pelo menos IMO.
Angew não está mais orgulhoso de SO
17
De fato. Esta não é uma otimização prematura. Em vez disso, é a maneira mais sensata de escrever essa função.
Lightness Races in Orbit
60

As operações de movimentação (como o construtor de movimentação) std::shared_ptrsão baratas , pois basicamente são "indicadores de roubo" (da origem para o destino; para ser mais preciso, todo o bloco de controle de estado é "roubado" da origem para o destino, incluindo as informações da contagem de referência) .

Em vez disso, as operações de cópia ao std::shared_ptrinvocar o aumento da contagem de referência atômica (ou seja, não apenas ++RefCountem um RefCountmembro de dados inteiro , mas, por exemplo, chamando o InterlockedIncrementWindows), que é mais caro do que apenas roubar ponteiros / estado.

Portanto, analisando a dinâmica da contagem de ref deste caso em detalhes:

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

Se você passar sppor valor e tirar uma cópia dentro do CompilerInstance::setInvocationmétodo, você terá:

  1. Ao inserir o método, o shared_ptrparâmetro é construído com cópia: ref count incremento atômico .
  2. Dentro do corpo do método, você copia o shared_ptrparâmetro no membro de dados: ref count incremento atômico .
  3. Ao sair do método, o shared_ptrparâmetro é destruído: ref count decrement atomic .

Você tem dois incrementos atômicos e um decréscimo atômico, para um total de três operações atômicas .

Em vez disso, se você passar o shared_ptrparâmetro por valor e depois std::movedentro do método (conforme feito corretamente no código de Clang), você terá:

  1. Ao inserir o método, o shared_ptrparâmetro é construído com cópia: ref count incremento atômico .
  2. Dentro do corpo do método, você std::moveo shared_ptrparâmetro no membro de dados: ref count não muda! Você está apenas roubando ponteiros / estado: não há operações caras de contagem atômica de ref.
  3. Ao sair do método, o shared_ptrparâmetro é destruído; mas desde que você avançou na etapa 2, não há nada para destruir, pois o shared_ptrparâmetro não está mais apontando para nada. Novamente, nenhum decréscimo atômico acontece neste caso.

Conclusão: neste caso, você obtém apenas um incremento atômico de contagem de ref, ou seja, apenas uma operação atômica .
Como você pode ver, isso é muito melhor do que dois incrementos atômicos mais um decréscimo atômico (para um total de três operações atômicas) para o caso da cópia.

Mr.C64
fonte
1
Também vale a pena notar: por que eles simplesmente não passam por referência const e evitam todo o material std :: move? Como a passagem por valor também permite que você passe um ponteiro bruto diretamente e haverá apenas um shared_ptr criado.
Joseph Ireland
@JosephIreland Porque você não pode mover uma referência de const #
Bruno Ferreira
2
@ JosephphIreland, porque se você chamar assim compilerInstance.setInvocation(std::move(sp));, não haverá incremento . Você pode obter o mesmo comportamento adicionando uma sobrecarga que requer uma shared_ptr<>&&duplicação, mas por que duplicar quando não é necessário.
ratchet freak
2
@BrunoFerreira Eu estava respondendo minha própria pergunta. Você não precisaria movê-lo porque é uma referência, apenas copie-o. Ainda apenas uma cópia em vez de duas. A razão pela qual eles não fazem isso é porque copiam desnecessariamente os shared_ptrs recém-construídos, por exemplo setInvocation(new CompilerInvocation), de ou como mencionado catraca setInvocation(std::move(sp)),. Desculpe se meu primeiro comentário não foi claro, eu realmente o publiquei por acidente, antes de terminar de escrever, e decidi simplesmente deixá-lo.
Joseph Ireland
22

Copiar a shared_ptrenvolve copiar seu ponteiro de objeto de estado interno e alterar a contagem de referência. Movê-lo envolve apenas a troca de ponteiros para o contador de referência interno e o objeto de propriedade, por isso é mais rápido.

SingerOfTheFall
fonte
16

Há duas razões para usar std :: move nessa situação. A maioria das respostas abordou a questão da velocidade, mas ignorou a questão importante de mostrar a intenção do código com mais clareza.

Para um std :: shared_ptr, std :: move indica inequivocamente uma transferência de propriedade do apontador, enquanto uma operação de cópia simples adiciona um proprietário adicional. Obviamente, se o proprietário original posteriormente abandonar sua propriedade (como permitir que seu std :: shared_ptr seja destruído), uma transferência de propriedade será realizada.

Quando você transfere a propriedade com std :: move, é óbvio o que está acontecendo. Se você usar uma cópia normal, não é óbvio que a operação pretendida é uma transferência até que você verifique se o proprietário original renuncia imediatamente à propriedade. Como bônus, é possível uma implementação mais eficiente, uma vez que uma transferência atômica de propriedade pode evitar o estado temporário em que o número de proprietários aumentou em um (e o assistente muda nas contagens de referência).

Stephen C. Steel
fonte
Exatamente o que estou procurando. Surpreendeu como outras respostas ignoram essa importante diferença semântica. os indicadores inteligentes são sobre propriedade.
Qweruiop