Existe um equivalente não atômico de std :: shared_ptr? E por que não há um em <memory>?

90

Esta é uma questão de duas partes, tudo sobre a atomicidade de std::shared_ptr:

1. Pelo que eu posso dizer, std::shared_ptré o único ponteiro inteligente <memory>que é atômico. Gostaria de saber se há uma versão não atômica do std::shared_ptrdisponível (não consigo ver nada no <memory>, então também estou aberto a sugestões fora do padrão, como aquelas no Boost). Eu sei que boost::shared_ptrtambém é atômico (se BOOST_SP_DISABLE_THREADSnão estiver definido), mas talvez haja outra alternativa? Estou procurando por algo que tenha a mesma semântica std::shared_ptr, mas sem a atomicidade.

2. Eu entendo porque std::shared_ptré atômico; é meio legal. No entanto, não é bom para todas as situações, e C ++ tem historicamente o mantra de "pague apenas pelo que usar". Se eu não estiver usando vários threads, ou se eu estiver usando vários threads, mas não estou compartilhando a propriedade do ponteiro entre os threads, um ponteiro inteligente atômico é um exagero. Minha segunda pergunta é por que uma versão não atômica do std::shared_ptrC ++ 11 não foi fornecida ? (assumindo que há um porquê ) (se a resposta for simplesmente "uma versão não atômica simplesmente nunca foi considerada" ou "ninguém nunca pediu uma versão não atômica", tudo bem!).

Com a pergunta nº 2, estou me perguntando se alguém já propôs uma versão não atômica de shared_ptr(para Boost ou o comitê de padrões) (não para substituir a versão atômica de shared_ptr, mas para coexistir com ela) e foi rejeitada por um Razão específica.

Talos de milho
fonte
4
Com qual "custo" exatamente você está preocupado aqui? O custo de incrementar atomicamente um número inteiro? Esse é realmente um custo que o preocupa para qualquer aplicação real? Ou você está apenas otimizando prematuramente?
Nicol Bolas
9
@NicolBolas: É mais curiosidade do que qualquer outra coisa; Não tenho (atualmente) nenhum código / projeto em que pretenda usar um ponteiro compartilhado não atômico. Porém, tive projetos (no passado) onde o Boost's shared_ptrteve uma desaceleração significativa devido à sua atomicidade, e a definição BOOST_DISABLE_THREADSfez uma diferença notável (não sei se std::shared_ptrteria o mesmo custo que boost::shared_ptrteve).
Cornstalks de
13
@Close eleitores: que parte da pergunta não é construtiva? Se não houver um porquê específico para a segunda pergunta, tudo bem (um simples "não foi considerado" seria uma resposta válida o suficiente). Estou curioso para saber se existe uma razão / razão específica. E a primeira pergunta certamente é válida, eu diria. Se eu precisar esclarecer a questão ou fazer pequenos ajustes, por favor me avise. Mas não vejo como isso não seja construtivo.
Cornstalks
10
@Cornstalks Bem, provavelmente as pessoas não reagem tão bem a perguntas que podem facilmente descartar como "otimização prematura" , não importa quão válida, bem formulada ou relevante seja a pergunta, eu acho. Eu, pessoalmente, não vejo nenhuma razão para encerrar isso como não construtivo.
Christian Rau
13
(não posso escrever uma resposta agora que está fechada, portanto, comentar) Com o GCC, quando seu programa não usa vários threads shared_ptr, não usa operações atômicas para o refcount. Consulte (2) em gcc.gnu.org/ml/libstdc++/2007-10/msg00180.html para um patch para o GCC para permitir que a implementação não atômica seja usada mesmo em aplicativos multithread, para shared_ptrobjetos que não são compartilhados entre tópicos. Estou sentado nesse patch há anos, mas estou considerando finalmente enviá-lo para o GCC 4.9
Jonathan Wakely

Respostas:

107

1. Gostaria de saber se existe uma versão não atômica de std :: shared_ptr disponível

Não fornecido pela norma. Pode muito bem haver um fornecido por uma biblioteca de "terceiros". Na verdade, antes do C ++ 11 e antes do Boost, parecia que todos escreviam seu próprio smart pointer contado de referência (incluindo eu).

2. Minha segunda pergunta é por que uma versão não atômica de std :: shared_ptr não foi fornecida em C ++ 11?

Esta questão foi discutida na reunião de Rapperswil em 2010. O assunto foi apresentado por um Comentário do Órgão Nacional nº 20 da Suíça. Houve fortes argumentos em ambos os lados do debate, incluindo aqueles que você forneceu em sua pergunta. No entanto, no final da discussão, a votação foi esmagadora (mas não unânime) contra a adição de uma versão não sincronizada (não atômica) de shared_ptr.

Argumentos contra incluídos:

  • O código escrito com shared_ptr não sincronizado pode acabar sendo usado em código encadeado no futuro, acabando causando problemas de depuração sem aviso prévio.

  • Ter um shared_ptr "universal" que é o "caminho único" para trafegar na contagem de referência tem benefícios: Da proposta original :

    Possui o mesmo tipo de objeto, independentemente dos recursos usados, facilitando muito a interoperabilidade entre as bibliotecas, incluindo bibliotecas de terceiros.

  • O custo das atômicas, embora não seja zero, não é avassalador. O custo é mitigado pelo uso de construção de movimento e atribuição de movimento que não precisa usar operações atômicas. Essas operações são comumente usadas para vector<shared_ptr<T>>apagar e inserir.

  • Nada proíbe as pessoas de escreverem seu próprio smart pointer não atômico com contagem de referência se isso for realmente o que desejam fazer.

A palavra final do LWG em Rapperswil naquele dia foi:

Rejeite CH 20. Não há consenso para fazer uma mudança neste momento.

Howard Hinnant
fonte
7
Nossa, perfeito, obrigado pela informação! Esse é exatamente o tipo de informação que eu esperava encontrar.
Cornstalks de
> Has the same object type regardless of features used, greatly facilitating interoperability between libraries, including third-party libraries. esse é um raciocínio extremamente estranho. Bibliotecas de terceiros fornecerão seus próprios tipos de qualquer maneira, então por que importaria se eles os fornecessem na forma de std :: shared_ptr <CustomType>, std :: non_atomic_shared_ptr <CustomType>, etc? você sempre terá que adaptar seu código ao que a biblioteca retorna de qualquer maneira
Jean-Michaël Celerier
Isso é verdade no que diz respeito aos tipos específicos de biblioteca, mas a ideia é que também existem muitos lugares onde os tipos padrão aparecem em APIs de terceiros. Por exemplo, minha biblioteca pode levar a std::shared_ptr<std::string>algum lugar. Se a biblioteca de outra pessoa também aceita esse tipo, os chamadores podem passar as mesmas strings para nós dois sem a inconveniência ou sobrecarga de conversão entre representações diferentes, e isso é uma pequena vitória para todos.
Jack O'Connor
52

Howard já respondeu bem à pergunta e Nicol fez alguns bons pontos sobre os benefícios de ter um único tipo de ponteiro compartilhado padrão, em vez de muitos tipos incompatíveis.

Embora eu concorde totalmente com a decisão do comitê, acho que há algum benefício em usar um shared_ptrtipo do tipo não sincronizado em casos especiais , então investiguei o tópico algumas vezes.

Se eu não estiver usando vários threads, ou se eu estiver usando vários threads, mas não estou compartilhando a propriedade do ponteiro entre os threads, um ponteiro inteligente atômico é um exagero.

Com o GCC, quando o seu programa não usa vários threads, shared_ptr não usa ops atômicos para o refcount. Isso é feito atualizando as contagens de referência por meio de funções de wrapper que detectam se o programa é multithreaded (no GNU / Linux isso é feito simplesmente detectando se o programa está vinculado alibpthread.so ) e despacha para operações atômicas ou não atômicas de acordo.

Percebi há muitos anos que, como o GCC shared_ptr<T>é implementado em termos de uma __shared_ptr<T, _LockPolicy>classe base , é possível usar a classe base com a política de bloqueio de thread único, mesmo em código multithread, usando explicitamente __shared_ptr<T, __gnu_cxx::_S_single>. Infelizmente, como esse não era um caso de uso pretendido, não funcionava perfeitamente antes do GCC 4.9, e algumas operações ainda usavam as funções de wrapper e, portanto, despachavam para operações atômicas, embora você tenha solicitado explicitamente a _S_singlepolítica. Veja o ponto (2) em http://gcc.gnu.org/ml/libstdc++/2007-10/msg00180.htmlpara obter mais detalhes e um patch para o GCC para permitir que a implementação não atômica seja usada mesmo em aplicativos multithread. Fiquei sentado naquele patch por anos, mas finalmente o comprometi para o GCC 4.9, que permite usar um modelo de alias como este para definir um tipo de ponteiro compartilhado que não é seguro para thread, mas é um pouco mais rápido:

template<typename T>
  using shared_ptr_unsynchronized = std::__shared_ptr<T, __gnu_cxx::_S_single>;

Este tipo não seria interoperável com std::shared_ptr<T>e somente seria seguro para uso quando fosse garantido que os shared_ptr_unsynchronizedobjetos nunca seriam compartilhados entre threads sem sincronização adicional fornecida pelo usuário.

É claro que isso é completamente não portável, mas às vezes não tem problema. Com os hacks de pré-processador corretos, seu código ainda funcionaria bem com outras implementações, se shared_ptr_unsynchronized<T>fosse um apelido para shared_ptr<T>, seria um pouco mais rápido com o GCC.


Se você estiver usando um GCC anterior ao 4.9, poderá usá-lo adicionando as _Sp_counted_base<_S_single>especializações explícitas ao seu próprio código (e garantindo que ninguém instancie __shared_ptr<T, _S_single>sem incluir as especializações, para evitar violações de ODR.) Adicionar essas especializações de stdtipos é tecnicamente indefinido, mas seria funcionam na prática, porque neste caso não há diferença entre eu adicionar as especializações ao GCC ou você adicioná-las ao seu próprio código.

Jonathan Wakely
fonte
2
Basta saber, há um erro de digitação em seu exemplo de alias de modelo? Ou seja, acho que deveria ser shared_ptr_unsynchronized = std :: __ shared_ptr <. A propósito, testei isso hoje, em conjunto com std :: __ enable_shared_from_this e std :: __ weak_ptr, e parece funcionar bem (gcc 4.9 e gcc 5.2). Farei o perfil / desmontagem em breve para ver se de fato as operações atômicas são ignoradas.
Carl Cook
Detalhes incríveis! Recentemente eu enfrentei um problema, conforme descrito em esta questão , que, eventualmente, me fez olhar para o código-fonte de std::shared_ptr, std::__shared_ptr, __default_lock_policye tal. Essa resposta confirmou o que entendi do código.
Nawaz
21

Minha segunda pergunta é por que uma versão não atômica de std :: shared_ptr não foi fornecida em C ++ 11? (assumindo que existe um porquê).

Alguém poderia facilmente perguntar por que não existe um ponteiro intrusivo ou qualquer outra variação possível de ponteiros compartilhados que alguém possa ter.

O projeto shared_ptrdo Boost foi o de criar uma língua-franca padrão mínimo de indicadores inteligentes. Que, de modo geral, você pode simplesmente puxar da parede e usá-lo. É algo que pode ser usado em uma ampla variedade de aplicativos. Você pode colocá-lo em uma interface e, provavelmente, boas pessoas estarão dispostas a usá-lo.

O encadeamento só vai se tornar mais prevalente no futuro. Na verdade, com o passar do tempo, o encadeamento geralmente será um dos principais meios de obter desempenho. Exigir que o ponteiro inteligente básico faça o mínimo necessário para suportar o encadeamento facilita essa realidade.

Colocar no padrão meia dúzia de ponteiros inteligentes com pequenas variações entre eles, ou ainda pior, um ponteiro inteligente baseado em políticas, teria sido terrível. Todos escolheriam o ponteiro que mais gostassem e rejeitariam todos os outros. Ninguém seria capaz de se comunicar com mais ninguém. Seria como as situações atuais com strings C ++, onde cada um tem seu próprio tipo. Só que pior, porque a interoperação com strings é muito mais fácil do que a interoperação entre classes de ponteiros inteligentes.

Boost e, por extensão, o comitê, escolheram um ponteiro inteligente específico para usar. Fornecia um bom equilíbrio de recursos e era amplamente e comumente usado na prática.

std::vectortem algumas ineficiências em comparação com arrays nus em alguns casos extremos. Tem algumas limitações; alguns usuários realmente querem ter um limite rígido para o tamanho de a vector, sem usar um alocador de lançamento. No entanto, o comitê não planejou vectorser tudo para todos. Ele foi projetado para ser um bom padrão para a maioria dos aplicativos. Aqueles para quem não pode funcionar podem simplesmente escrever uma alternativa que atenda às suas necessidades.

Da mesma forma que você pode para um indicador inteligente se shared_ptra atomicidade de é um fardo. Então, novamente, você também pode considerar não copiá-los tanto.

Nicol Bolas
fonte
7
+1 para "também se pode considerar não copiá-los tanto".
Ali
Se você já instalou um profiler, você é especial e pode simplesmente ignorar argumentos como o acima. Se você não tem um requisito operacional difícil de atender, não deve usar C ++. Discutir como você faz é uma boa maneira de tornar o C ++ universalmente insultado por qualquer pessoa interessada em alto desempenho ou baixa latência. É por isso que os programadores de jogos não usam STL, boost ou mesmo exceções.
Hans Malherbe
Para maior clareza, acho que a citação no topo de sua resposta deveria ser "por que uma versão não atômica de std :: shared_ptr não foi fornecida em C ++ 11?"
Charles Savoie
4

Estou preparando uma palestra sobre shared_ptr no trabalho. Tenho usado um boost modificado shared_ptr com evitar malloc separado (como o que make_shared pode fazer) e um parâmetro de modelo para política de bloqueio como shared_ptr_unsynchronized mencionado acima. Estou usando o programa de

http://flyingfrogblog.blogspot.hk/2011/01/boosts-sharedptr-up-to-10-slower-than.html

como um teste, depois de limpar as cópias compartilhadas_ptr desnecessárias. O programa usa apenas o thread principal e o argumento de teste é mostrado. O env de teste é um notebook executando o Linuxmint 14. Aqui está o tempo gasto em segundos:

impulso de configuração de execução de teste (1.49) std com aumento modificado make_shared
mt-inseguro (11) 11,9 9 / 11,5 (-pthread ativado) 8,4  
atômico (11) 13,6 12,4 13,0  
mt-inseguro (12) 113,5 85,8 / 108,9 (-pthread ligado) 81,5  
atômico (12) 126,0 109,1 123,6  

Apenas a versão 'std' usa -std = cxx11, e o -pthread provavelmente muda a lock_policy na classe g ++ __shared_ptr.

A partir desses números, vejo o impacto das instruções atômicas na otimização do código. O caso de teste não usa nenhum contêiner C ++, mas vector<shared_ptr<some_small_POD>>provavelmente sofrerá se o objeto não precisar da proteção de thread. Boost sofre menos provavelmente porque o malloc adicional está limitando a quantidade de inlining e otimização de código.

Ainda estou para encontrar uma máquina com núcleos suficientes para testar a escalabilidade das instruções atômicas, mas usar std :: shared_ptr apenas quando necessário é provavelmente melhor.

russ
fonte
4

Boost fornece um shared_ptrque não é atômico. É chamado local_shared_ptre pode ser encontrado na biblioteca de smart pointers do boost.

The Quantum Physicist
fonte
+1 para uma resposta curta e sólida com boa citação, mas este tipo de ponteiro parece caro - em termos de memória e tempo de execução, devido a um nível extra de indireção (local-> compartilhado-> ptr vs compartilhado-> ptr).
Red.Wave
@ Red.Wave Você pode explicar o que quer dizer com indireção e como isso afeta o desempenho? Você quer dizer que é um shared_ptrcom um contador de qualquer maneira, embora seja local? Ou você quer dizer que há outro problema com isso? Os médicos dizem que a única diferença é que isso não é atômico.
The Quantum Physicist
Cada ptr local mantém uma contagem e referência ao ptr original compartilhado. Assim, qualquer acesso à ponta final precisa de um desvio de referência do ponto local para o ponto compartilhado, que é então desreferenciado para a ponta. Assim, há mais uma indireção empilhada às vias indiretas de ptr compartilhado. E isso aumenta a sobrecarga.
Red.Wave
@ Red.Wave De onde você está obtendo essas informações? Este: "Assim, qualquer acesso à ponta final precisa de uma desreferenciação do ponto local para o ponto compartilhado" precisa de alguma citação. Não consegui encontrar isso nos documentos boost. Novamente, o que vi nos documentos é que diz isso local_shared_ptre shared_ptrsão idênticos, exceto para atômicos. Estou genuinamente interessado em descobrir se o que você está dizendo é verdade porque eu uso local_shared_ptrem aplicativos que exigem alto desempenho.
The Quantum Physicist
3
@ Red.Wave Se você olhar para o código-fonte real github.com/boostorg/smart_ptr/blob/… você verá que não há indireção dupla. Este parágrafo da documentação é apenas um modelo mental.
Ilya Popov