O C ++ moderno pode obter desempenho de graça?

205

Às vezes, alega-se que o C ++ 11/14 pode proporcionar um aumento de desempenho, mesmo quando apenas compilando o código C ++ 98. A justificativa é geralmente na linha da semântica de movimentação, pois em alguns casos os construtores rvalue são gerados automaticamente ou agora fazem parte do STL. Agora, estou me perguntando se esses casos já foram realmente tratados pelo RVO ou otimizações de compilador semelhantes.

Minha pergunta então é se você poderia me dar um exemplo real de um pedaço de código C ++ 98 que, sem modificação, roda mais rápido usando um compilador que suporta os novos recursos de linguagem. Entendo que um compilador em conformidade padrão não é necessário para executar a cópia e, por esse motivo, a semântica do movimento pode gerar velocidade, mas eu gostaria de ver um caso menos patológico, se você desejar.

EDIT: Apenas para deixar claro, não estou perguntando se os novos compiladores são mais rápidos que os antigos, mas se existe um código pelo qual a adição de -std = c ++ 14 aos meus sinalizadores do compilador funcionaria mais rapidamente (evite cópias, mas se você pode inventar qualquer outra coisa além da semântica de movimentos, eu também estaria interessado)

um grande
fonte
3
Lembre-se de que a elisão de cópia e a otimização do valor de retorno são executadas ao construir um novo objeto usando um construtor de cópia. No entanto, em um operador de atribuição de cópias, não há elisão de cópias (como pode ser, pois o compilador não sabe o que fazer com um objeto já construído que não é temporário). Portanto, nesse caso, o C ++ 11/14 ganha muito, oferecendo a possibilidade de usar um operador de atribuição de movimentação. Porém, sobre a sua pergunta, não acho que o código C ++ 98 deva ser mais rápido se compilado por um compilador C ++ 11/14, talvez seja mais rápido porque o compilador é mais novo.
vsoftco 22/12
27
O código que usa a biblioteca padrão é potencialmente mais rápido, mesmo se você o tornar totalmente compatível com o C ++ 98, porque no C ++ 11/14 a biblioteca subjacente usa a semântica de movimentação interna quando possível. Portanto, o código que parece idêntico no C ++ 98 e no C ++ 11/14 será (possivelmente) mais rápido no último caso, sempre que você usar os objetos de biblioteca padrão, como vetores, listas etc. e mover semântica, fará a diferença.
Vsoftco
1
@vsoftco, Esse é o tipo de situação que eu estava me referindo, mas não consegui dar um exemplo: Pelo que me lembro, se eu tiver que definir o construtor de cópia, o construtor de movimentação não será gerado automaticamente, o que nos deixa com aulas muito simples onde RVO, eu acho, sempre funciona. Uma exceção pode ser algo em conjunto com os contêineres STL, onde os construtores rvalue são gerados pelo implementador da biblioteca (o que significa que eu não precisaria alterar nada no código para usar movimentos).
Alarge
As classes não precisam ser simples para não ter um construtor de cópias. O C ++ prospera na semântica de valores e o construtor de cópias, o operador de atribuição, o destruidor etc. devem ser a exceção.
Sp2danny
1
@ Eric Obrigado pelo link, foi interessante. No entanto, tendo analisado rapidamente, as vantagens de velocidade parecem vir principalmente da adição std::movee movimentação de construtores (o que exigiria modificações no código existente). A única coisa realmente relacionada à minha pergunta foi a frase "Você obtém vantagens imediatas de velocidade simplesmente recompilando", que não é apoiada por nenhum exemplo (ela menciona STL no mesmo slide, como fiz na minha pergunta, mas nada específico ) Eu estava pedindo alguns exemplos. Se estiver lendo os slides incorretamente, informe-me.
alarge

Respostas:

221

Estou ciente de 5 categorias gerais em que a recompilação de um compilador C ++ 03, pois o C ++ 11 pode causar aumentos ilimitados de desempenho que praticamente não têm relação com a qualidade da implementação. Todas essas são variações da semântica de movimentação.

std::vector realocar

struct bar{
  std::vector<int> data;
};
std::vector<bar> foo(1);
foo.back().data.push_back(3);
foo.reserve(10); // two allocations and a delete occur in C++03

cada vez que o fooda memória intermédia é realocada em C ++ 03 que copiados cada vectorem bar.

No C ++ 11, ele move os bar::datas, que são basicamente livres.

Nesse caso, isso depende de otimizações dentro do stdcontêiner vector. Em todos os casos abaixo, o uso de stdcontêineres é apenas porque eles são objetos C ++ que possuem movesemântica eficiente no C ++ 11 "automaticamente" quando você atualiza seu compilador. Objetos que não o bloqueiam e que contêm um stdcontêiner também herdam o aprimoramento automáticomove construtores .

Falha no NRVO

Quando o NRVO (otimização do valor de retorno nomeado) falha, no C ++ 03 ele volta à cópia, no C ++ 11 volta ao movimento. As falhas do NRVO são fáceis:

std::vector<int> foo(int count){
  std::vector<int> v; // oops
  if (count<=0) return std::vector<int>();
  v.reserve(count);
  for(int i=0;i<count;++i)
    v.push_back(i);
  return v;
}

ou até:

std::vector<int> foo(bool which) {
  std::vector<int> a, b;
  // do work, filling a and b, using the other for calculations
  if (which)
    return a;
  else
    return b;
}

Temos três valores - o valor de retorno e dois valores diferentes dentro da função. O Elision permite que os valores dentro da função sejam 'mesclados' com o valor de retorno, mas não entre si. Ambos não podem ser mesclados com o valor de retorno sem mesclar um ao outro.

A questão básica é que a elisão do NRVO é frágil e o código com alterações fora do returnsite pode repentinamente ter reduções de desempenho maciças naquele local sem diagnóstico emitido. Na maioria dos casos de falha de NRVO, o C ++ 11 termina com a move, enquanto o C ++ 03 termina com uma cópia.

Retornando um Argumento de Função

Elision também é impossível aqui:

std::set<int> func(std::set<int> in){
  return in;
}

no C ++ 11 isso é barato: no C ++ 03 não há como evitar a cópia. Argumentos para funções não podem ser elididos com o valor retornado, porque a vida útil e a localização do parâmetro e o valor retornado são gerenciados pelo código de chamada.

No entanto, o C ++ 11 pode passar de um para o outro. (Em um exemplo de menos brinquedo, algo pode ser feito para o set).

push_back ou insert

Finalmente, a elisão em contêineres não acontece: mas o C ++ 11 sobrecarrega o rvalue move operadores de inserção, o que salva cópias.

struct whatever {
  std::string data;
  int count;
  whatever( std::string d, int c ):data(d), count(c) {}
};
std::vector<whatever> v;
v.push_back( whatever("some long string goes here", 3) );

no C ++ 03, um temporário whateveré criado e, em seguida, copiado para o vetor v. 2 std::stringbuffers são alocados, cada um com dados idênticos, e um é descartado.

No C ++ 11, um temporário whateveré criado. A whatever&& push_backsobrecarga moveé temporária no vetor v. Um std::stringbuffer é alocado e movido para o vetor. Um vazio std::stringé descartado.

Tarefa

Roubado da resposta de @ Jarod42 abaixo.

A elisão não pode ocorrer com a atribuição, mas a mudança pode.

std::set<int> some_function();

std::set<int> some_value;

// code

some_value = some_function();

aqui some_functionretorna um candidato para o qual fugir, mas como ele não é usado para construir um objeto diretamente, ele não pode ser escolhido. No C ++ 03, o acima resulta no conteúdo do temporário sendo copiado some_value. No C ++ 11, ele é movido para some_value, o que basicamente é gratuito.


Para o efeito completo do exposto, você precisa de um compilador que sintetize construtores de movimento e atribuição para você.

O MSVC 2013 implementa os construtores de movimentação em stdcontêineres, mas não sintetiza os construtores de movimentação nos seus tipos.

Portanto, tipos contendo std::vectors e similares não obtêm essas melhorias no MSVC2013, mas começarão a obtê-los no MSVC2015.

clang e gcc já implementaram construtores de movimentação implícitos. O compilador de 2013 da Intel oferecerá suporte à geração implícita de construtores de movimentação, se você passar -Qoption,cpp,--gen_move_operations(eles não fazem isso por padrão em um esforço para serem compatíveis com o MSVC2013).

Yakk - Adam Nevraumont
fonte
1
@alarge yes. Porém, para que um construtor de movimentação seja muitas vezes mais eficiente que um construtor de cópias, ele geralmente precisa mover recursos em vez de copiá-los. Sem escrever seus próprios construtores de movimentação (e apenas recompilar um programa C ++ 03), todos os stdcontêineres da biblioteca serão atualizados com os moveconstrutores "de graça" e (se você não o bloqueou) construções que usam os referidos objetos ( e os referidos objetos) começarão a obter a construção de movimentação livre em várias situações. Muitas dessas situações são cobertas pela elision no C ++ 03: nem todas.
Yakk - Adam Nevraumont
5
Essa é uma péssima implementação do otimizador, pois, como os objetos com nomes diferentes retornados não têm vida útil sobreposta, o RVO ainda é teoricamente possível.
Ben Voigt
2
@alarge Há lugares onde a elisão falha, como quando dois objetos com vida útil sobreposta podem ser unidos em um terceiro, mas não um no outro. Em seguida, é necessário mover no C ++ 11 e copiar no C ++ 03 (ignorando como se). Elision é frequentemente frágil na prática. O uso dos stdcontêineres acima se deve principalmente ao fato de serem baratos demais para se mover para copiar o tipo que você recebe 'de graça' no C ++ 11 ao recompilar o C ++ 03. A vector::resizeé uma exceção: ele usa moveem C ++ 11.
usar o seguinte comando
27
Eu vejo apenas uma categoria geral que é a semântica de movimentos e cinco casos especiais disso.
Johannes Schaub - litb
3
@sebro Eu entendo, você não considera que "faz com que os programas não aloquem muitos milhares de alocações de kilobytes e, em vez disso, mova os ponteiros" para ser suficiente. Você deseja resultados cronometrados. Microbenchmarks não são mais provas de melhorias de desempenho do que a prova de que você está fazendo fundamentalmente menos. Menos de 100 aplicativos do mundo real em uma ampla variedade de setores sendo perfilados com o perfil de tarefas do mundo real não é realmente uma prova. Tomei vagas afirmações sobre "desempenho livre" e fiz fatos específicos sobre diferenças no comportamento do programa em C ++ 03 e C ++ 11.
Yakk - Adam Nevraumont 14/01/19
46

se você tiver algo como:

std::vector<int> foo(); // function declaration.
std::vector<int> v;

// some code

v = foo();

Você obteve uma cópia no C ++ 03, enquanto uma atribuição de movimentação no C ++ 11. então você tem otimização gratuita nesse caso.

Jarod42
fonte
4
@Yakk: Como ocorre a cópia elision na atribuição?
Jarod42
2
@ Jarod42 Eu também acredito que a remoção de cópias não é possível em uma tarefa, uma vez que o lado esquerdo já está construído e não há uma maneira razoável de um compilador saber o que fazer com os dados "antigos" depois de roubar os recursos da direita lado da mão. Mas talvez eu esteja errado, eu adoraria descobrir de uma vez por todas a resposta. A cópia elision faz sentido quando você copia a construção, pois o objeto é "novo" e não há problema em decidir o que fazer com os dados antigos. Até onde eu sei, a única exceção é esta: "As atribuições só podem ser elididas com base na regra como se"
vsoftco
4
Boa C ++ 03 código já fez um movimento neste caso, viafoo().swap(v);
Ben Voigt
@BenVoigt com certeza, mas nem todo o código é otimizado, e nem todos os locais onde isso acontece são fáceis de alcançar.
usar o seguinte comando
A cópia ellision pode funcionar em uma tarefa, como @BenVoigt diz. Melhor termo é RVO (otimização do valor de retorno) e só funciona se foo () tiver sido implementado dessa maneira.
DrumM