Qual é a sobrecarga de ponteiros inteligentes em comparação com ponteiros normais em C ++?

101

Qual é a sobrecarga de ponteiros inteligentes em comparação com ponteiros normais em C ++ 11? Em outras palavras, meu código ficará mais lento se eu usar ponteiros inteligentes e, em caso afirmativo, quanto mais lento?

Especificamente, estou perguntando sobre o C ++ 11 std::shared_ptre std::unique_ptr.

Obviamente, as coisas empurradas para baixo na pilha serão maiores (pelo menos eu acho), porque um ponteiro inteligente também precisa armazenar seu estado interno (contagem de referência, etc), a questão realmente é, quanto isso vai afetam meu desempenho, se afetam?

Por exemplo, eu retorno um ponteiro inteligente de uma função em vez de um ponteiro normal:

std::shared_ptr<const Value> getValue();
// versus
const Value *getValue();

Ou, por exemplo, quando uma das minhas funções aceita um ponteiro inteligente como parâmetro em vez de um ponteiro normal:

void setValue(std::shared_ptr<const Value> val);
// versus
void setValue(const Value *val);
Venemo
fonte
8
A única maneira de saber é avaliar seu código.
Basile Starynkevitch
Qual deles voce quer dizer? std::unique_ptrou std::shared_ptr?
stefan,
10
A resposta é 42. (outras palavras, quem sabe, você precisa traçar o perfil de seu código e entender seu hardware para sua carga de trabalho típica.)
Nim
Seu aplicativo precisa fazer uso extremo de ponteiros inteligentes para ser significativo.
user2672165
O custo de usar um shared_ptr em uma função setter simples é terrível e adicionará um overhead múltiplo de 100%.
Lothar

Respostas:

176

std::unique_ptr tem sobrecarga de memória apenas se você fornecê-lo com algum deletador não trivial.

std::shared_ptr sempre tem sobrecarga de memória para o contador de referência, embora seja muito pequeno.

std::unique_ptr tem sobrecarga de tempo apenas durante o construtor (se ele tiver que copiar o deleter fornecido e / ou inicializar nulo o ponteiro) e durante o destruidor (para destruir o objeto de propriedade).

std::shared_ptrtem sobrecarga de tempo no construtor (para criar o contador de referência), no destruidor (para diminuir o contador de referência e possivelmente destruir o objeto) e no operador de atribuição (para incrementar o contador de referência). Devido às garantias de segurança de thread std::shared_ptr, esses incrementos / decréscimos são atômicos, adicionando mais sobrecarga.

Observe que nenhum deles tem uma sobrecarga de tempo na desreferenciação (ao obter a referência ao objeto possuído), enquanto esta operação parece ser a mais comum para ponteiros.

Para resumir, há alguma sobrecarga, mas não deve tornar o código lento, a menos que você crie e destrua ponteiros inteligentes continuamente.

lisaro
fonte
11
unique_ptrnão tem sobrecarga no destruidor. Ele faz exatamente o mesmo que faria com um ponteiro bruto.
R. Martinho Fernandes
6
@R.MartinhoFernandes comparando com o próprio ponteiro bruto, ele tem uma sobrecarga de tempo no destruidor, já que o destruidor de ponteiro bruto não faz nada. Comparando com a forma como um ponteiro bruto provavelmente seria usado, certamente não tem sobrecarga.
lisaro
3
Vale a pena notar que parte do custo de construção / destruição / atribuição de shared_ptr se deve à segurança do thread
Joe,
1
Além disso, e quanto ao construtor padrão de std::unique_ptr? Se você construir um std::unique_ptr<int>, o interno int*será inicializado, nullptrquer você goste ou não.
Martin Drozdik
1
@MartinDrozdik Na maioria das situações, você inicializaria nulo o ponteiro bruto também, para verificar sua nulidade mais tarde, ou algo parecido. No entanto, adicionado isto à resposta, obrigado.
lisyarus,
26

Como acontece com todo o desempenho do código, o único meio realmente confiável de obter informações concretas é medir e / ou inspecionar o código de máquina.

Dito isso, o raciocínio simples diz que

  • Você pode esperar alguma sobrecarga em compilações de depuração, uma vez que, por exemplo, operator->deve ser executado como uma chamada de função para que você possa entrar nela (isso, por sua vez, é devido à falta geral de suporte para marcar classes e funções como não depuradas).

  • Pois shared_ptrvocê pode esperar alguma sobrecarga na criação inicial, uma vez que envolve a alocação dinâmica de um bloco de controle, e a alocação dinâmica é muito mais lenta do que qualquer outra operação básica em C ++ (use make_sharedquando for praticamente possível, para minimizar essa sobrecarga).

  • Além disso, shared_ptrhá alguma sobrecarga mínima na manutenção de uma contagem de referência, por exemplo, ao passar um shared_ptrvalor por, mas não há sobrecarga para unique_ptr.

Mantendo o primeiro ponto acima em mente, ao medir, faça isso tanto para compilações de depuração quanto de liberação.

O comitê internacional de padronização C ++ publicou um relatório técnico sobre o desempenho , mas isso foi em 2006, antes unique_ptre shared_ptrfoi adicionado à biblioteca padrão. Ainda assim, as dicas inteligentes eram ultrapassadas naquele ponto, então o relatório também considerou isso. Citando a parte relevante:

“Se acessar um valor por meio de um ponteiro inteligente trivial for significativamente mais lento do que acessá-lo por meio de um ponteiro comum, o compilador está lidando com a abstração de maneira ineficiente. No passado, a maioria dos compiladores tinha penalidades de abstração significativas e vários compiladores atuais ainda têm. No entanto, foi relatado que pelo menos dois compiladores têm penalidades de abstração abaixo de 1% e outro uma penalidade de 3%, portanto, eliminar esse tipo de sobrecarga está bem dentro do estado da arte ”

Como um palpite bem informado, o “bem dentro do estado da arte” foi alcançado com os compiladores mais populares de hoje, desde o início de 2014.

Saúde e hth. - Alf
fonte
Você poderia incluir alguns detalhes em sua resposta sobre os casos que adicionei à minha pergunta?
Venemo
Isso pode ter sido verdade há 10 ou mais anos, mas hoje, inspecionar o código de máquina não é tão útil quanto a pessoa acima sugere. Dependendo de como as instruções são pipeline, vetorizadas, ... e como o compilador / processador lida com a especulação, em última análise, é o quão rápido ela é. Menos código de máquina de código não significa necessariamente código mais rápido. A única maneira de determinar o desempenho é traçando seu perfil. Isso pode mudar com base no processador e também por compilador.
Byron
Um problema que vi é que, uma vez que shared_ptrs são usados ​​em um servidor, o uso de shared_ptrs começa a proliferar, e logo shared_ptrs se torna a técnica de gerenciamento de memória padrão. Portanto, agora você repetiu as penalidades de abstração de 1-3%, que são aplicadas repetidamente.
Nathan Doromal
Acho que comparar uma compilação de depuração é uma perda de tempo completa e absoluta
Paul Childs
26

Minha resposta é diferente das outras e realmente me pergunto se eles já criaram um perfil de código.

shared_ptr tem uma sobrecarga significativa para a criação por causa de sua alocação de memória para o bloco de controle (que mantém o contador ref e uma lista de ponteiros para todas as referências fracas). Ele também tem uma grande sobrecarga de memória por causa disso e do fato de que std :: shared_ptr é sempre uma tupla de 2 ponteiros (um para o objeto, um para o bloco de controle).

Se você passar um ponteiro_compartilhado para uma função como um parâmetro de valor, ele será pelo menos 10 vezes mais lento que uma chamada normal e criará muitos códigos no segmento de código para o desenrolar da pilha. Se você passar por referência, terá uma indireção adicional que também pode ser bem pior em termos de desempenho.

É por isso que você não deve fazer isso, a menos que a função esteja realmente envolvida no gerenciamento de propriedade. Caso contrário, use "shared_ptr.get ()". Ele não foi projetado para garantir que seu objeto não seja morto durante uma chamada de função normal.

Se você enlouquecer e usar shared_ptr em pequenos objetos como uma árvore de sintaxe abstrata em um compilador ou em pequenos nós em qualquer outra estrutura de gráfico, você verá uma grande queda no desempenho e um grande aumento na memória. Eu vi um sistema analisador que foi reescrito logo após o C ++ 14 chegar ao mercado e antes que o programador aprendesse a usar ponteiros inteligentes corretamente. A reescrita foi uma magnitude mais lenta do que o código antigo.

Não é uma bala de prata e os indicadores brutos também não são ruins por definição. Programadores ruins são ruins e design ruim é ruim. Projete com cuidado, projete com propriedade clara em mente e tente usar o shared_ptr principalmente no limite da API do subsistema.

Se você quiser saber mais, pode assistir a uma boa palestra de Nicolai M. Josuttis sobre "O preço real dos ponteiros compartilhados em C ++" https://vimeo.com/131189627
Ele se aprofunda nos detalhes de implementação e arquitetura de CPU para barreiras de gravação, atômica depois de ouvir, você nunca mais vai falar sobre esse recurso ser barato. Se você quiser apenas uma prova da magnitude mais lenta, pule os primeiros 48 minutos e observe-o executando o código de exemplo que é executado até 180 vezes mais lento (compilado com -O3) ao usar o ponteiro compartilhado em todos os lugares.

Lothar
fonte
Obrigado pela sua resposta! Em qual plataforma você fez o perfil? Você pode respaldar suas reivindicações com alguns dados?
Venemo
Não tenho nenhum número para mostrar, mas você pode encontrar alguns em Nico Josuttis talk vimeo.com/131189627
Lothar
6
Já ouviu falar std::make_shared()? Além disso, acho as demonstrações de mau uso flagrante sendo um pouco chatas ...
Deduplicator
2
Tudo o que "make_shared" pode fazer é protegê-lo de uma alocação adicional e fornecer um pouco mais de localidade de cache se o bloco de controle for alocado na frente do objeto. Não pode deixar de ajudar quando você passa o ponteiro ao redor. Esta não é a raiz dos problemas.
Lothar
14

Em outras palavras, meu código ficará mais lento se eu usar ponteiros inteligentes e, em caso afirmativo, quanto mais lento?

Mais devagar? Provavelmente não, a menos que você esteja criando um índice enorme usando shared_ptrs e não tenha memória suficiente a ponto de seu computador começar a enrugar, como uma senhora que cai no chão por uma força insuportável de longe.

O que tornaria seu código mais lento são pesquisas lentas, processamento de loop desnecessário, cópias enormes de dados e muitas operações de gravação em disco (como centenas).

As vantagens de um ponteiro inteligente estão todas relacionadas ao gerenciamento. Mas a sobrecarga é necessária? Isso depende da sua implementação. Digamos que você esteja iterando em uma matriz de 3 fases, cada fase possui uma matriz de 1024 elementos. Criar um smart_ptrpara esse processo pode ser um exagero, pois, uma vez que a iteração for concluída, você saberá que terá que apagá-lo. Assim, você pode ganhar memória extra por não usar um smart_ptr...

Mas você realmente quer fazer isso?

Um único vazamento de memória pode fazer com que seu produto tenha um ponto de falha no tempo (digamos que seu programa vaze 4 megabytes a cada hora, levaria meses para quebrar um computador, no entanto, ele vai quebrar, você sabe porque o vazamento está aí) .

É como dizer "seu software tem garantia de 3 meses, então me chame para o serviço."

Então, no final, é realmente uma questão de ... você pode lidar com esse risco? Usar um ponteiro bruto para manipular sua indexação em centenas de objetos diferentes vale a pena perder o controle da memória.

Se a resposta for sim, use um ponteiro bruto.

Se você nem mesmo quiser considerar isso, a smart_ptré uma solução boa, viável e incrível.

Claudiordgz
fonte
4
ok, mas valgrind é bom para verificar possíveis vazamentos de memória, então contanto que você o use, você deve estar seguro ™
graywolf
@Paladin Sim, se você pode controlar sua memória, smart_ptrsão realmente úteis para grandes equipes
Claudiordgz
3
Eu uso unique_ptr, ele simplifica muitas coisas, mas não gosto de shared_ptr, contagem de referência não é muito eficiente GC e também não é perfeito
graywolf
1
@Paladin eu tento usar ponteiros brutos se eu puder encapsular tudo. Se for algo que vou espalhar por todo o lugar como uma discussão, então talvez eu considere um smart_ptr. A maioria dos meus unique_ptrs são usados ​​na grande implementação, como um método principal ou de execução
Claudiordgz
@Lothar Vejo que você parafraseou uma das coisas que eu disse em sua resposta: Thats why you should not do this unless the function is really involved in ownership management... ótima resposta, obrigado, votou positivamente
Claudiordgz
0

Apenas para dar uma olhada e apenas para o []operador, é ~ 5X mais lento do que o ponteiro bruto, conforme demonstrado no código a seguir, que foi compilado usando gcc -lstdc++ -std=c++14 -O0e gerou este resultado:

malloc []:     414252610                                                 
unique []  is: 2062494135                                                
uq get []  is: 238801500                                                 
uq.get()[] is: 1505169542
new is:        241049490 

Estou começando a aprender c ++, tenho isso em mente: você sempre precisa saber o que está fazendo e dedicar mais tempo para saber o que os outros fizeram em seu c ++.

EDITAR

Conforme mencionado por @Mohan Kumar, forneci mais detalhes. A versão do gcc é 7.4.0 (Ubuntu 7.4.0-1ubuntu1~14.04~ppa1), O resultado acima foi obtido quando o -O0é usado, no entanto, quando eu uso o sinalizador '-O2', eu tenho o seguinte:

malloc []:     223
unique []  is: 105586217
uq get []  is: 71129461
uq.get()[] is: 69246502
new is:        9683

Em seguida, mudou para clang version 3.9.0, -O0foi:

malloc []:     409765889
unique []  is: 1351714189
uq get []  is: 256090843
uq.get()[] is: 1026846852
new is:        255421307

-O2 foi:

malloc []:     150
unique []  is: 124
uq get []  is: 83
uq.get()[] is: 83
new is:        54

O resultado do clang -O2é incrível.

#include <memory>
#include <iostream>
#include <chrono>
#include <thread>

uint32_t n = 100000000;
void t_m(void){
    auto a  = (char*) malloc(n*sizeof(char));
    for(uint32_t i=0; i<n; i++) a[i] = 'A';
}
void t_u(void){
    auto a = std::unique_ptr<char[]>(new char[n]);
    for(uint32_t i=0; i<n; i++) a[i] = 'A';
}

void t_u2(void){
    auto a = std::unique_ptr<char[]>(new char[n]);
    auto tmp = a.get();
    for(uint32_t i=0; i<n; i++) tmp[i] = 'A';
}
void t_u3(void){
    auto a = std::unique_ptr<char[]>(new char[n]);
    for(uint32_t i=0; i<n; i++) a.get()[i] = 'A';
}
void t_new(void){
    auto a = new char[n];
    for(uint32_t i=0; i<n; i++) a[i] = 'A';
}

int main(){
    auto start = std::chrono::high_resolution_clock::now();
    t_m();
    auto end1 = std::chrono::high_resolution_clock::now();
    t_u();
    auto end2 = std::chrono::high_resolution_clock::now();
    t_u2();
    auto end3 = std::chrono::high_resolution_clock::now();
    t_u3();
    auto end4 = std::chrono::high_resolution_clock::now();
    t_new();
    auto end5 = std::chrono::high_resolution_clock::now();
    std::cout << "malloc []:     " <<  (end1 - start).count() << std::endl;
    std::cout << "unique []  is: " << (end2 - end1).count() << std::endl;
    std::cout << "uq get []  is: " << (end3 - end2).count() << std::endl;
    std::cout << "uq.get()[] is: " << (end4 - end3).count() << std::endl;
    std::cout << "new is:        " << (end5 - end4).count() << std::endl;
}
liqg3
fonte
Eu testei o código agora, ele é apenas 10% lento ao usar o ponteiro exclusivo.
Mohan Kumar
8
nunca, jamais, faça benchmarks -O0ou depure códigos. A saída será extremamente ineficiente . Sempre use pelo menos -O2(ou -O3hoje em dia porque alguma vetorização não é feita -O2)
phuclv
1
Se você tiver tempo e quiser uma pausa para o café, tome -O4 para obter a otimização do tempo de link e todas as pequenas funções de abstração ficam embutidas e desaparecem.
Lothar
Você deve incluir uma freechamada no teste malloc, e delete[]para novo (ou tornar a variável aestática), porque os unique_ptrs estão chamando por delete[]baixo do capô, em seus destruidores.
RnMss