O uso de 'auto' do C ++ 11 pode melhorar o desempenho?

230

Eu posso ver por que o autotipo em C ++ 11 melhora a correção e a manutenção. Eu li que ele também pode melhorar o desempenho ( Quase sempre automático de Herb Sutter), mas sinto falta de uma boa explicação.

  • Como pode automelhorar o desempenho?
  • Alguém pode dar um exemplo?
DaBrain
fonte
5
Veja herbsutter.com/2013/06/13/… que fala sobre como evitar conversões implícitas acidentais, por exemplo, de gadget para widget. Não é um problema comum.
Jonathan Wakely
42
Você aceita "reduz a probabilidade de pessimizar involuntariamente" como uma melhoria de desempenho?
Sep12
1
Perfomance de código de limpeza no futuro só, talvez
Croll
Precisamos de uma resposta curta: não, se você é bom. Pode evitar erros "noobish". O C ++ possui uma curva de aprendizado que mata quem não consegue, afinal.
Alec Teal

Respostas:

309

autopode ajudar no desempenho, evitando conversões implícitas silenciosas . Um exemplo que considero atraente é o seguinte.

std::map<Key, Val> m;
// ...

for (std::pair<Key, Val> const& item : m) {
    // do stuff
}

Vê o bug? Aqui estamos, pensando que estamos usando elegantemente todos os itens do mapa por referência const e usando a nova expressão range-for para tornar nossa intenção clara, mas na verdade estamos copiando todos os elementos. Isto é porque std::map<Key, Val>::value_typeé std::pair<const Key, Val>, não std::pair<Key, Val>. Assim, quando nós (implicitamente) temos:

std::pair<Key, Val> const& item = *iter;

Em vez de pegar uma referência a um objeto existente e deixá-lo assim, precisamos fazer uma conversão de tipo. Você tem permissão para fazer uma referência const a um objeto (ou temporário) de um tipo diferente, desde que haja uma conversão implícita disponível, por exemplo:

int const& i = 2.0; // perfectly OK

A conversão de tipo é uma conversão implícita permitida pelo mesmo motivo pelo qual você pode converter a const Keyem a Key, mas precisamos construir um temporário do novo tipo para permitir isso. Assim, efetivamente nosso loop:

std::pair<Key, Val> __tmp = *iter;       // construct a temporary of the correct type
std::pair<Key, Val> const& item = __tmp; // then, take a reference to it

(É claro que não há realmente um __tmpobjeto, ele está lá apenas para ilustração; na realidade, o temporário sem nome está vinculado apenas itempor toda a sua vida).

Apenas alterando para:

for (auto const& item : m) {
    // do stuff
}

nos salvou uma tonelada de cópias - agora o tipo referenciado corresponde ao tipo do inicializador, portanto, nenhuma conversão ou temporária é necessária, podemos apenas fazer uma referência direta.

Barry
fonte
19
@ Barry Você pode explicar por que o compilador faria cópias felizes em vez de reclamar sobre tentar tratar um std::pair<const Key, Val> const &como um std::pair<Key, Val> const &? Novo no C ++ 11, não tenho certeza de como o range-for e se autoencaixa nisso.
Agop 10/09/2015
@ Barry Obrigado pela explicação. Essa é a peça que eu estava perdendo - por algum motivo, pensei que você não pudesse ter uma referência constante a um temporário. Mas é claro que você pode - isso deixará de existir no final de seu escopo.
Agop 10/09/15
@ Barry, eu entendo você, mas o problema é que, então, não há uma resposta que cubra todos os motivos para usar autoesse aumento de desempenho. Então, eu vou escrever com minhas próprias palavras abaixo.
Yakk - Adam Nevraumont 10/09
38
Ainda acho que isso não prova que " automelhora o desempenho". É apenas um exemplo que " autoajuda a evitar erros de programadores que destroem o desempenho". Eu afirmo que há uma distinção sutil, mas importante, entre os dois. Ainda assim, +1.
Lightness Races in Orbit
70

Como autodeduz o tipo da expressão de inicialização, não há conversão de tipo envolvida. Combinado com algoritmos de modelo, isso significa que você pode obter uma computação mais direta do que se você fosse criar um tipo - especialmente quando estiver lidando com expressões cujo tipo você não pode nomear!

Um exemplo típico vem de (ab) usando std::function:

std::function<bool(T, T)> cmp1 = std::bind(f, _2, 10, _1);  // bad
auto cmp2 = std::bind(f, _2, 10, _1);                       // good
auto cmp3 = [](T a, T b){ return f(b, 10, a); };            // also good

std::stable_partition(begin(x), end(x), cmp?);

Com cmp2e cmp3, todo o algoritmo pode alinhar a chamada de comparação, enquanto que, se você construir um std::functionobjeto, a chamada não pode ser incorporada, mas também é necessário passar pela pesquisa polimórfica no interior do invólucro da função que foi apagado por tipo.

Outra variante desse tema é que você pode dizer:

auto && f = MakeAThing();

Essa é sempre uma referência, vinculada ao valor da expressão de chamada de função e nunca constrói nenhum objeto adicional. Se você não conhecia o tipo do valor retornado, pode ser forçado a construir um novo objeto (talvez temporário) por meio de algo como T && f = MakeAThing(). (Além disso, auto &&funciona mesmo quando o tipo de retorno não é móvel e o valor de retorno é um pré-valor.)

Kerrek SB
fonte
Portanto, este é o motivo de "evitar o apagamento do tipo" auto. Sua outra variante é "evitar cópias acidentais", mas precisa de detalhes; por que autovocê acelera simplesmente digitando o tipo lá? (Acho que a resposta é "você errou o tipo e ele silenciosamente se converte"). O que o torna um exemplo menos bem explicado da resposta de Barry, não? Ou seja, existem dois casos básicos: automático para evitar o apagamento de tipo e automático para evitar erros de tipo silencioso que são convertidos acidentalmente, ambos com custo de tempo de execução.
Yakk - Adam Nevraumont 10/09
2
"não apenas a chamada não pode ser incorporada" - por que é isso então? Quer dizer que no impede algo princípio da chamada que está sendo devirtualized depois de fluxo de dados de análise, se as especializações relevantes de std::bind, std::functione std::stable_partitiontodos foram inlined? Ou apenas que, na prática, nenhum compilador C ++ será incorporado de forma agressiva o suficiente para resolver a bagunça?
21715 Steve Jobs (
@SteveJessop: Principalmente o último - depois que você passar pelo std::functionconstrutor, será muito complexo ver a chamada real, especialmente com otimizações de pequenas funções (para que você realmente não deseje desirtualização). Claro que, em princípio, tudo é como se ...
Kerrek SB
41

Existem duas categorias.

autopode evitar o apagamento do tipo. Existem tipos inomináveis ​​(como lambdas) e tipos quase inomináveis ​​(como o resultado de std::bindou outro modelo de expressão semelhante a coisas).

Sem auto, você acaba digitando apagar os dados para algo parecido std::function. O apagamento do tipo tem custos.

std::function<void()> task1 = []{std::cout << "hello";};
auto task2 = []{std::cout << " world\n";};

task1possui sobrecarga de apagamento de tipo - uma possível alocação de heap, dificuldade em incluí-la e sobrecarga de invocação de tabela de função virtual. task2não tem nenhum. Lambdas precisam de deduções automáticas ou outras formas de dedução de tipo para armazenar sem apagamento de tipo; outros tipos podem ser tão complexos que só precisam dele na prática.

Segundo, você pode errar tipos. Em alguns casos, o tipo errado funcionará aparentemente perfeitamente, mas causará uma cópia.

Foo const& f = expression();

irá compilar se expression()retornos Bar const&ouBar mesmo Bar&, de onde Foopode ser construído Bar. Um temporário Fooserá criado, depois vinculado a f, e sua vida útil será prolongada até fdesaparecer.

O programador pode ter pretendido Bar const& fe não ter a intenção de fazer uma cópia lá, mas uma cópia é feita independentemente.

O exemplo mais comum é o tipo de *std::map<A,B>::const_iterator , que std::pair<A const, B> const&não é std::pair<A,B> const&, mas o erro é uma categoria de erros que silenciosamente custam desempenho. Você pode construir a std::pair<A, B>partir destd::pair<const A, B> . (A chave em um mapa é const, porque editá-lo é uma má ideia)

Tanto o @Barry quanto o @KerrekSB ilustraram esses dois princípios em suas respostas. Isso é simplesmente uma tentativa de destacar as duas questões em uma resposta, com uma redação que visa o problema, em vez de ser centrada no exemplo.

Yakk - Adam Nevraumont
fonte
9

As três respostas existentes fornecem exemplos em que o uso de autoajuda "torna menos provável a pessiminação involuntária" tornando-o "melhor desempenho".

Há um outro lado da moeda. O uso autocom objetos que possuem operadores que não retornam o objeto básico pode resultar em código incorreto (ainda compilável e executável). Por exemplo, esta pergunta pergunta como o uso autodeu resultados diferentes (incorretos) usando a biblioteca Eigen, ou seja , as seguintes linhas

const auto    resAuto    = Ha + Vector3(0.,0.,j * 2.567);
const Vector3 resVector3 = Ha + Vector3(0.,0.,j * 2.567);

std::cout << "resAuto = " << resAuto <<std::endl;
std::cout << "resVector3 = " << resVector3 <<std::endl;

resultou em saída diferente. É certo que isso se deve principalmente à avaliação lenta do Eigens, mas esse código é / deve ser transparente para o usuário (da biblioteca).

Embora o desempenho não tenha sido muito afetado aqui, o uso autopara evitar pessimização não intencional pode ser classificado como otimização prematura ou, pelo menos, errado;).

Avi Ginsburg
fonte
1
Adicionado a pergunta oposta: stackoverflow.com/questions/38415831/...
Leon