Versão curta: é comum retornar objetos grandes - como vetores / matrizes - em muitas linguagens de programação. Este estilo agora é aceitável em C ++ 0x se a classe tiver um construtor de movimento, ou os programadores de C ++ o consideram estranho / feio / abominação?
Versão longa: Em C ++ 0x isso ainda é considerado formato incorreto?
std::vector<std::string> BuildLargeVector();
...
std::vector<std::string> v = BuildLargeVector();
A versão tradicional seria assim:
void BuildLargeVector(std::vector<std::string>& result);
...
std::vector<std::string> v;
BuildLargeVector(v);
Na versão mais recente, o valor retornado de BuildLargeVector
é um rvalue, então v seria construído usando o construtor de movimento de std::vector
, supondo que (N) RVO não ocorra.
Mesmo antes de C ++ 0x, a primeira forma costumava ser "eficiente" devido ao (N) RVO. No entanto, (N) RVO fica a critério do compilador. Agora que temos referências de rvalue, é garantido que nenhuma cópia profunda ocorrerá.
Edit : A questão realmente não é sobre otimização. Ambos os formulários mostrados têm desempenho quase idêntico em programas do mundo real. Considerando que, no passado, a primeira forma poderia ter tido um desempenho pior da ordem de magnitude. Como resultado, a primeira forma foi por muito tempo um grande cheiro de código na programação C ++. Não é mais, espero?
Respostas:
Dave Abrahams tem uma análise bastante abrangente da velocidade de passagem / retorno de valores .
Resposta curta, se você precisar retornar um valor, retorne um valor. Não use referências de saída porque o compilador faz isso de qualquer maneira. Claro que existem ressalvas, então você deve ler esse artigo.
fonte
x / 2
parax >> 1
forint
s, mas você supõe que sim. O padrão também não diz nada sobre como os compiladores são obrigados a implementar referências, mas você assume que eles são tratados com eficiência usando ponteiros. O padrão também não diz nada sobre as tabelas v, então você também não pode ter certeza de que as chamadas de funções virtuais são eficientes. Essencialmente, às vezes você precisa colocar um pouco de fé no compilador.Pelo menos IMO, geralmente é uma ideia ruim, mas não por razões de eficiência. É uma ideia ruim porque a função em questão geralmente deve ser escrita como um algoritmo genérico que produz sua saída por meio de um iterador. Quase todo código que aceita ou retorna um contêiner em vez de operar em iteradores deve ser considerado suspeito.
Não me interpretem mal: há momentos em que faz sentido passar objetos semelhantes a coleções (por exemplo, strings), mas para o exemplo citado, consideraria passar ou retornar o vetor uma ideia ruim.
fonte
A essência é:
Copy Elision e RVO podem evitar as "cópias assustadoras" (o compilador não é necessário para implementar essas otimizações e, em algumas situações, não pode ser aplicado)
As referências C ++ 0x RValue permitem implementações de string / vetor que garantem isso.
Se você pode abandonar compiladores / implementações STL mais antigos, retorne vetores livremente (e certifique-se de que seus próprios objetos também os suportem). Se sua base de código precisa suportar compiladores "menores", mantenha o estilo antigo.
Infelizmente, isso tem grande influência em suas interfaces. Se C ++ 0x não for uma opção e você precisar de garantias, poderá usar, em vez disso, objetos de contagem de referência ou de cópia na gravação em alguns cenários. Eles têm desvantagens com multithreading, no entanto.
(Eu gostaria que apenas uma resposta em C ++ fosse simples, direta e sem condições).
fonte
Na verdade, desde o C ++ 11, o custo de cópia do C ++
std::vector
acabou na maioria dos casos.No entanto, deve-se ter em mente que o custo de construir o novo vetor (e então destruí- lo) ainda existe, e usar parâmetros de saída em vez de retornar por valor ainda é útil quando você deseja reutilizar a capacidade do vetor. Isso é documentado como uma exceção em F.20 das Diretrizes Básicas C ++.
Vamos comparar:
com:
Agora, suponha que precisemos chamar esses métodos
numIter
em um loop apertado e executar alguma ação. Por exemplo, vamos calcular a soma de todos os elementos.Usando
BuildLargeVector1
, você faria:Usando
BuildLargeVector2
, você faria:No primeiro exemplo, há muitas alocações / desalocações dinâmicas desnecessárias acontecendo, que são evitadas no segundo exemplo usando um parâmetro de saída da maneira antiga, reutilizando a memória já alocada. Se essa otimização vale ou não a pena, depende do custo relativo da alocação / desalocação em comparação com o custo de calcular / alterar os valores.
Benchmark
Vamos brincar com os valores de
vecSize
enumIter
. Manteremos vecSize * numIter constante para que "em teoria" demore o mesmo tempo (= há o mesmo número de atribuições e adições, com exatamente os mesmos valores), e a diferença de tempo só pode vir do custo de alocações, desalocações e melhor uso do cache.Mais especificamente, vamos usar vecSize * numIter = 2 ^ 31 = 2147483648, porque eu tenho 16 GB de RAM e esse número garante que não mais do que 8 GB sejam alocados (sizeof (int) = 4), garantindo que não estou trocando para o disco ( todos os outros programas foram fechados, eu tinha ~ 15 GB disponíveis ao executar o teste).
Aqui está o código:
E aqui está o resultado:
(Intel i7-7700K @ 4,20 GHz; 16 GB DDR4 2400 MHz; Kubuntu 18,04)
Notação: mem (v) = v.size () * sizeof (int) = v.size () * 4 na minha plataforma.
Não surpreendentemente, quando
numIter = 1
(isto é, mem (v) = 8GB), os tempos são perfeitamente idênticos. De fato, em ambos os casos, estamos alocando apenas uma vez um grande vetor de 8 GB na memória. Isso também prova que nenhuma cópia aconteceu ao usar BuildLargeVector1 (): Eu não teria RAM suficiente para fazer a cópia!Quando
numIter = 2
, reutilizar a capacidade do vetor em vez de realocar um segundo vetor é 1,37x mais rápido.Quando
numIter = 256
, reutilizar a capacidade do vetor (em vez de alocar / desalocar um vetor repetidamente 256 vezes ...) é 2,45x mais rápido :)Podemos notar que time1 é praticamente constante de
numIter = 1
anumIter = 256
, o que significa que alocar um grande vetor de 8 GB é quase tão caro quanto alocar 256 vetores de 32 MB. No entanto, alocar um vetor enorme de 8 GB é definitivamente mais caro do que alocar um vetor de 32 MB, portanto, reutilizar a capacidade do vetor fornece ganhos de desempenho.De
numIter = 512
(mem (v) = 16 MB) anumIter = 8M
(mem (v) = 1kB) é o ponto ideal: ambos os métodos são exatamente tão rápidos e mais rápidos do que todas as outras combinações de numIter e vecSize. Isso provavelmente tem a ver com o fato de que o tamanho do cache L3 do meu processador é de 8 MB, de modo que o vetor cabe quase completamente no cache. Eu realmente não explico por que o salto repentino detime1
é para mem (v) = 16 MB, pareceria mais lógico acontecer logo depois, quando mem (v) = 8 MB. Observe que, surpreendentemente, neste ponto ideal, não reutilizar a capacidade é, na verdade, um pouco mais rápido! Eu realmente não explico isso.Quando as
numIter > 8M
coisas começam a ficar feias. Ambos os métodos ficam mais lentos, mas o retorno do vetor por valor fica ainda mais lento. No pior caso, com um vetor contendo apenas umint
, reutilizar a capacidade em vez de retornar por valor é 3,3x mais rápido. Presumivelmente, isso se deve aos custos fixos de malloc () que começam a dominar.Observe como a curva para o tempo2 é mais suave do que a curva para o tempo1: não apenas reutilizar a capacidade do vetor é geralmente mais rápido, mas talvez o mais importante, é mais previsível .
Observe também que, no ponto ideal, fomos capazes de realizar 2 bilhões de adições de inteiros de 64 bits em ~ 0,5s, o que é bastante ideal em um processador de 64 bits de 4,2 GHz. Poderíamos fazer melhor paralelizando a computação para usar todos os 8 núcleos (o teste acima usa apenas um núcleo por vez, o que eu verifiquei reexecutando o teste enquanto monitora o uso da CPU). O melhor desempenho é obtido quando mem (v) = 16kB, que é a ordem de magnitude do cache L1 (o cache de dados L1 para o i7-7700K é 4x32kB).
É claro que as diferenças se tornam cada vez menos relevantes quanto mais cálculos você realmente precisa fazer nos dados. Abaixo estão os resultados se substituirmos
sum = std::accumulate(v.begin(), v.end(), sum);
porfor (int k : v) sum += std::sqrt(2.0*k);
:Conclusões
Os resultados podem ser diferentes em outras plataformas. Como de costume, se o desempenho for importante, escreva benchmarks para seu caso de uso específico.
fonte
Ainda acho que é uma prática ruim, mas é importante notar que minha equipe usa MSVC 2008 e GCC 4.1, portanto, não estamos usando os compiladores mais recentes.
Anteriormente, muitos dos pontos de acesso mostrados no vtune com MSVC 2008 se resumiam à cópia de strings. Tínhamos um código como este:
... observe que usamos nosso próprio tipo String (isso foi necessário porque estamos fornecendo um kit de desenvolvimento de software em que os criadores de plug-ins podem estar usando compiladores diferentes e, portanto, implementações diferentes e incompatíveis de std :: string / std :: wstring).
Fiz uma mudança simples em resposta à sessão de criação de perfil de amostragem de gráfico de chamada mostrando que String :: String (const String &) estava ocupando uma quantidade significativa de tempo. Métodos como no exemplo acima foram os maiores contribuidores (na verdade, a sessão de criação de perfil mostrou a alocação e desalocação de memória como um dos maiores pontos de acesso, com o construtor de cópia String sendo o principal contribuidor para as alocações).
A mudança que fiz foi simples:
No entanto, isso fez uma grande diferença! O ponto de acesso foi embora nas sessões subsequentes do criador de perfil e, além disso, fazemos muitos testes de unidade completos para acompanhar o desempenho do nosso aplicativo. Todos os tipos de tempos de teste de desempenho caíram significativamente após essas mudanças simples.
Conclusão: não estamos usando os compiladores mais recentes absolutos, mas ainda não podemos depender do compilador otimizando a cópia para retornar por valor de forma confiável (pelo menos não em todos os casos). Esse pode não ser o caso para aqueles que usam compiladores mais novos como o MSVC 2010. Estou ansioso para ver quando poderemos usar C ++ 0x e simplesmente usar referências rvalue e nunca teremos que nos preocupar se estamos pessimizando nosso código retornando complex classes por valor.
[Editar] Como Nate apontou, RVO se aplica ao retorno de temporários criados dentro de uma função. No meu caso, não havia tais temporários (exceto para o ramo inválido onde construímos uma string vazia) e, portanto, RVO não teria sido aplicável.
fonte
<::
ou??!
com o operador condicional?:
(às vezes chamado de operador ternário ).Apenas para criticar um pouco: não é comum em muitas linguagens de programação retornar arrays de funções. Na maioria deles, uma referência a array é retornada. Em C ++, a analogia mais próxima seria retornar
boost::shared_array
fonte
shared_ptr
e dê um basta no dia.Se o desempenho é um problema real, você deve perceber que a semântica de movimentação nem sempre é mais rápida do que copiar. Por exemplo, se você tem uma string que usa a otimização de string pequena , para strings pequenas, um construtor de movimento deve fazer exatamente a mesma quantidade de trabalho que um construtor de cópia regular.
fonte