Existe uma diferença de desempenho entre i ++ e ++ i em C ++?

Nós temos a pergunta existe uma diferença de desempenho entre i++ e ++i em C ?

Qual a resposta para o C ++?

[Sumário Executivo: Use ++i se você não tiver um motivo específico para usar o i++ .]

Para C ++, a resposta é um pouco mais complicada.

Se i é um tipo simples (não uma instância de uma class C ++), então a resposta dada para C (“Não, não há diferença de desempenho”) é válida, uma vez que o compilador está gerando o código.

No entanto, se i for uma instância de uma class C ++, então i++ e ++i estão fazendo chamadas para uma das funções do operator++ . Aqui está um par padrão dessas funções:

 Foo& Foo::operator++() // called for ++i { this->data += 1; return *this; } Foo Foo::operator++(int ignored_dummy_value) // called for i++ { Foo tmp(*this); // variable "tmp" cannot be optimized away by the compiler ++(*this); return tmp; } 

Como o compilador não está gerando código, mas apenas chamando uma function de operator++ , não há como otimizar a variável tmp e seu construtor de cópia associado. Se o construtor de cópia for caro, isso pode ter um impacto significativo no desempenho.

Sim. Há sim.

O operador ++ pode ou não ser definido como uma function. Para tipos primitivos (int, double, …) os operadores são integrados, portanto, o compilador provavelmente será capaz de otimizar seu código. Mas no caso de um object que define o operador ++, as coisas são diferentes.

O operador ++ (int) function deve criar uma cópia. Isso ocorre porque se espera que o postfix ++ retorne um valor diferente do que ele contém: ele deve manter seu valor em uma variável temporária, incrementar seu valor e retornar o valor temporário. No caso do operador ++ (), prefixo ++, não há necessidade de criar uma cópia: o object pode se incrementar e depois simplesmente retornar a si mesmo.

Aqui está uma ilustração do ponto:

 struct C { C& operator++(); // prefix C operator++(int); // postfix private: int i_; }; C& C::operator++() { ++i_; return *this; // self, no copy created } CC::operator++(int ignored_dummy_value) { C t(*this); ++(*this); return t; // return a copy } 

Toda vez que você chama o operador ++ (int), você deve criar uma cópia, e o compilador não pode fazer nada sobre isso. Quando dada a escolha, use o operador ++ (); Desta forma, você não salva uma cópia. Pode ser significativo no caso de muitos incrementos (loop grande?) E / ou objects grandes.

Aqui está uma referência para o caso quando os operadores de incremento estão em diferentes unidades de tradução. Compilador com g ++ 4.5.

Ignore os problemas de estilo por enquanto

 // a.cc #include  #include  class Something { public: Something& operator++(); Something operator++(int); private: std::array data; }; int main () { Something s; for (int i=0; i<1024*1024*30; ++i) ++s; // warm up std::clock_t a = clock(); for (int i=0; i<1024*1024*30; ++i) ++s; a = clock() - a; for (int i=0; i<1024*1024*30; ++i) s++; // warm up std::clock_t b = clock(); for (int i=0; i<1024*1024*30; ++i) s++; b = clock() - b; std::cout << "a=" << (a/double(CLOCKS_PER_SEC)) << ", b=" << (b/double(CLOCKS_PER_SEC)) << '\n'; return 0; } 

O (n) incremento

Teste

 // b.cc #include  class Something { public: Something& operator++(); Something operator++(int); private: std::array data; }; Something& Something::operator++() { for (auto it=data.begin(), end=data.end(); it!=end; ++it) ++*it; return *this; } Something Something::operator++(int) { Something ret = *this; ++*this; return ret; } 

Resultados

Resultados (os tempos estão em segundos) com o g ++ 4.5 em uma máquina virtual:

 Flags (--std=c++0x) ++i i++ -DPACKET_SIZE=50 -O1 1.70 2.39 -DPACKET_SIZE=50 -O3 0.59 1.00 -DPACKET_SIZE=500 -O1 10.51 13.28 -DPACKET_SIZE=500 -O3 4.28 6.82 

O (1) incremento

Teste

Vamos agora pegar o seguinte arquivo:

 // c.cc #include  class Something { public: Something& operator++(); Something operator++(int); private: std::array data; }; Something& Something::operator++() { return *this; } Something Something::operator++(int) { Something ret = *this; ++*this; return ret; } 

Não faz nada no incremento. Isso simula o caso quando a incremento tem complexidade constante.

Resultados

Os resultados agora variam muito:

 Flags (--std=c++0x) ++i i++ -DPACKET_SIZE=50 -O1 0.05 0.74 -DPACKET_SIZE=50 -O3 0.08 0.97 -DPACKET_SIZE=500 -O1 0.05 2.79 -DPACKET_SIZE=500 -O3 0.08 2.18 -DPACKET_SIZE=5000 -O3 0.07 21.90 

Conclusão

Performance-wise

Se você não precisa do valor anterior, crie o hábito de usar o pré-incremento. Seja consistente mesmo com os tipos internos, você se acostumará a ele e não correrá o risco de sofrer uma perda desnecessária de desempenho se replace um tipo embutido por um tipo personalizado.

Semântica

  • i++ diz increment i, I am interested in the previous value, though .
  • ++i digo increment i, I am interested in the current value ou increment i, no interest in the previous value . Mais uma vez, você vai se acostumar com isso, mesmo que você não esteja agora.

Knuth

Otimização prematura é a raiz de todo o mal. Como é a pessimização prematura.

Não é totalmente correto dizer que o compilador não pode otimizar a cópia da variável temporária no caso do postfix. Um teste rápido com o VC mostra que, pelo menos, isso pode ser feito em certos casos.

No exemplo a seguir, o código gerado é idêntico para o prefixo e o postfix, por exemplo:

 #include  class Foo { public: Foo() { myData=0; } Foo(const Foo &rhs) { myData=rhs.myData; } const Foo& operator++() { this->myData++; return *this; } const Foo operator++(int) { Foo tmp(*this); this->myData++; return tmp; } int GetData() { return myData; } private: int myData; }; int main(int argc, char* argv[]) { Foo testFoo; int count; printf("Enter loop count: "); scanf("%d", &count); for(int i=0; i 

Quer você faça ++ testFoo ou testFoo ++, você ainda obterá o mesmo código resultante. De fato, sem ler a contagem do usuário, o otimizador reduziu a coisa toda a uma constante. Então, é isso:

 for(int i=0; i<10; i++) { testFoo++; } printf("Value: %d\n", testFoo.GetData()); 

Resultou no seguinte:

 00401000 push 0Ah 00401002 push offset string "Value: %d\n" (402104h) 00401007 call dword ptr [__imp__printf (4020A0h)] 

Então, embora seja certamente o caso de a versão do postfix poder ser mais lenta, pode ser que o otimizador seja bom o suficiente para se livrar da cópia temporária se você não a estiver usando.

O Guia de Estilo do Google C ++ diz:

Pré-incremento e pré-incremento

Use o formulário de prefixo (++ i) dos operadores de incremento e decremento com iteradores e outros objects de modelo.

Definição: Quando uma variável é incrementada (++ i ou i ++) ou decrementada (–i ou i–) e o valor da expressão não é usado, deve-se decidir se pré-incremento (decremento) ou pós-incremento (decremento).

Prós: Quando o valor de retorno é ignorado, o formulário “pre” (++ i) nunca é menos eficiente que o formulário “post” (i ++) e é geralmente mais eficiente. Isso ocorre porque o pós-incremento (ou decremento) requer que uma cópia de i seja feita, que é o valor da expressão. Se eu for um iterador ou outro tipo não-escalar, copiar eu poderia ser caro. Como os dois tipos de incremento se comportam da mesma forma quando o valor é ignorado, por que não apenas pré-incrementar?

Contras: A tradição desenvolvida, em C, de usar pós-incremento quando o valor da expressão não é usado, especialmente em loops for. Alguns acham que o pós-incremento é mais fácil de ler, já que o “sujeito” (i) precede o “verbo” (++), assim como em inglês.

Decisão: Para valores escalar simples (não object), não há razão para preferir um formulário e também permitimos um. Para iteradores e outros tipos de modelo, use pré-incremento.

Gostaria de salientar recentemente um excelente post de Andrew Koenig sobre Code Talk.

http://dobbscodetalk.com/index.php?option=com_myblog&show=Efficiency-versus-intent.html&Itemid=29

Na nossa empresa também usamos convenção de ++ iter para consistência e desempenho, quando aplicável. Mas Andrew levanta detalhes sobre a intenção versus desempenho. Há momentos em que queremos usar o iter ++ em vez do ++ iter.

Então, primeiro decida a sua intenção e se pre ou post não importa, então vá com pre, pois ele terá algum benefício de desempenho, evitando a criação de objects extras e jogando-os.

@Ketan

… levanta detalhes sobre a intenção versus desempenho. Há momentos em que queremos usar o iter ++ em vez do ++ iter.

Obviamente post e pre-increment têm diferentes semânticas e tenho certeza que todos concordam que quando o resultado é usado você deve usar o operador apropriado. Eu acho que a questão é o que se deve fazer quando o resultado é descartado (como nos loops for ). A resposta a esta pergunta (IMHO) é que, uma vez que as considerações de desempenho são insignificantes na melhor das hipóteses, você deve fazer o que é mais natural. For myself ++i é mais natural, mas minha experiência me diz que estou em minoria e usar o i++ causará menos sobrecarga de metal para a maioria das pessoas que lêem seu código.

Afinal, esta é a razão pela qual a linguagem não é chamada ” ++C “. [*]

[*] Insira discussão obrigatória sobre o ++C ser um nome mais lógico.

Mark: Só queria salientar que os operadores ++ são bons candidatos para serem embutidos, e se o compilador optar por fazê-lo, a cópia redundante será eliminada na maioria dos casos. (por exemplo, tipos POD, que geralmente são iteradores.)

Dito isso, ainda é melhor usar o estilo ++ na maioria dos casos. 🙂

A diferença de desempenho entre ++i e i++ será mais aparente quando você pensar em operadores como funções de retorno de valor e como eles são implementados. Para facilitar a compreensão do que está acontecendo, os exemplos de código a seguir usarão int como se fosse uma struct .

++i incrementa a variável e retorna o resultado. Isso pode ser feito no local e com o mínimo de tempo de CPU, exigindo apenas uma linha de código em muitos casos:

 int& int::operator++() { return *this += 1; } 

Mas o mesmo não pode ser dito de i++ .

Pós-incremento, i++ , é visto como retornando o valor original antes de incrementar. No entanto, uma function só pode retornar um resultado quando terminar . Como resultado, torna-se necessário criar uma cópia da variável contendo o valor original, incrementar a variável e, em seguida, retornar a cópia que contém o valor original:

 int int::operator++(int& _Val) { int _Original = _Val; _Val += 1; return _Original; } 

Quando não há diferença funcional entre pré-incremento e pós-incremento, o compilador pode realizar a otimização de forma que não haja diferença de desempenho entre os dois. No entanto, se um tipo de dados composto, como uma struct ou class estiver envolvido, o construtor de cópia será chamado no pós-incremento, e não será possível realizar essa otimização se uma cópia profunda for necessária. Como tal, o pré-incremento geralmente é mais rápido e requer menos memory que o pós-incremento.

  1. ++ i – mais rápido não usando o valor de retorno
  2. i ++ – mais rápido usando o valor de retorno

Quando não estiver usando o valor de retorno, o compilador tem a garantia de não usar um temporário no caso de ++ i . Não é garantido que seja mais rápido, mas garantido que não seja mais lento.

Ao usar o valor de retorno, o i ++ permite que o processador empurre tanto o incremento quanto o lado esquerdo para o pipeline, já que eles não dependem um do outro. ++ i pode parar o pipeline porque o processador não pode iniciar o lado esquerdo até que a operação de pré-incremento tenha percorrido todo o caminho. Mais uma vez, uma parada de pipeline não é garantida, já que o processador pode encontrar outras coisas úteis.

@Mark: Eu apaguei a minha resposta anterior porque foi um pouco difícil, e merecia um downvote para isso sozinho. Eu realmente acho que é uma boa pergunta no sentido de que pergunta o que está na mente de muitas pessoas.

A resposta usual é que ++ i é mais rápido que i ++, e sem dúvida é, mas a questão maior é “quando você deve se importar?”

Se a fração de tempo de CPU gasto no incremento de iteradores for menor que 10%, talvez você não se importe.

Se a fração do tempo de CPU gasto no incremento de iteradores for maior que 10%, você poderá verificar quais instruções estão fazendo essa iteração. Veja se você poderia apenas incrementar inteiros em vez de usar iteradores. As chances são que você poderia, e embora possa ser de alguma forma menos desejável, as chances são muito boas que você vai economizar essencialmente todo o tempo gasto nesses iteradores.

Eu vi um exemplo em que o incremento de iterador estava consumindo bem mais de 90% do tempo. Nesse caso, ir para incremento de número inteiro reduziu o tempo de execução em essencialmente esse valor. (ou seja, melhor que 10x de aceleração)

A pergunta pretendida era sobre quando o resultado não é utilizado (isso está claro na pergunta para C). Alguém pode consertar isso já que a questão é “wiki da comunidade”?

Sobre otimizações prematuras, Knuth é frequentemente citado. Está certo. mas Donald Knuth nunca defenderia com esse código horrível que você pode ver nestes dias. Já viu a = b + c entre Java Integers (não int)? Isso equivale a 3 conversões de boxe / unboxing. Evitar coisas assim é importante. E escrevendo inutilmente o i ++ em vez do ++ i é o mesmo erro. EDIT: Como phresnel bem coloca em um comentário, isso pode ser resumido como “otimização prematura é mal, como é pessimização prematura”.

Mesmo o fato de que as pessoas estão mais acostumadas com o i ++ é um infeliz legado C causado por um erro conceitual da K & R (se você seguir o argumento intencional, é uma conclusão lógica; e defender K & R porque eles são sem sentido, eles são ótimo, mas eles não são ótimos como designers de linguagem, existem incontáveis ​​erros no design C, desde gets () a strcpy (), até a API strncpy () (ela deve ter tido a API strlcpy () desde o dia 1) ).

Btw, eu sou um daqueles não utilizados o suficiente para C + + para encontrar + + eu irritante de ler. Ainda assim, eu uso isso desde que reconheço que está certo.

@wilhelmtell

O compilador pode elidir o temporário. Verbatim do outro segmento:

O compilador C ++ pode eliminar temporárias baseadas em pilha, mesmo que isso altere o comportamento do programa. Link do MSDN para o VC 8:

http://msdn.microsoft.com/en-us/library/ms364057(VS.80).aspx

A razão pela qual você deve usar o ++ i mesmo em tipos incorporados onde não há vantagem de desempenho é criar um bom hábito para si mesmo.

Ambos são tão rápidos;) Se você quiser, é o mesmo cálculo para o processador, é apenas a ordem em que é feito que diferem.

Por exemplo, o código a seguir:

 #include  int main() { int a = 0; a++; int b = 0; ++b; return 0; } 

Produza a seguinte assembly:

  0x0000000100000f24 : push %rbp 0x0000000100000f25 : mov %rsp,%rbp 0x0000000100000f28 : movl $0x0,-0x4(%rbp) 0x0000000100000f2f : incl -0x4(%rbp) 0x0000000100000f32 : movl $0x0,-0x8(%rbp) 0x0000000100000f39 : incl -0x8(%rbp) 0x0000000100000f3c : mov $0x0,%eax 0x0000000100000f41 : leaveq 0x0000000100000f42 : retq 

Você vê que para um ++ e um b ++ é um mnemônico incluso, então é a mesma operação;)

Tempo para fornecer às pessoas gemas de sabedoria;) – existe um truque simples para fazer o incremento de postfix do C ++ se comportar praticamente como o acréscimo de prefixo (Inventado isso para mim, mas o vi também no código de outras pessoas, então não estou sozinho).

Basicamente, o truque é usar a class auxiliar para adiar o incremento após o retorno, e o RAII vem para resgatar

 #include  class Data { private: class DataIncrementer { private: Data& _dref; public: DataIncrementer(Data& d) : _dref(d) {} public: ~DataIncrementer() { ++_dref; } }; private: int _data; public: Data() : _data{0} {} public: Data(int d) : _data{d} {} public: Data(const Data& d) : _data{ d._data } {} public: Data& operator=(const Data& d) { _data = d._data; return *this; } public: ~Data() {} public: Data& operator++() { // prefix ++_data; return *this; } public: Data operator++(int) { // postfix DataIncrementer t(*this); return *this; } public: operator int() { return _data; } }; int main() { Data d(1); std::cout << d << '\n'; std::cout << ++d << '\n'; std::cout << d++ << '\n'; std::cout << d << '\n'; return 0; } 

Inventado é para algum código pesado de iteradores personalizados e reduz o tempo de execução. O custo do prefixo vs postfix é uma referência agora, e se este for o operador customizado que está fazendo movimentação pesada, o prefixo e o postfix geraram o mesmo tempo de execução para mim.

Quando você escreve i++ você está dizendo ao compilador para incrementar depois de terminar esta linha ou loop.

++i é um pouco diferente do que i++ . Em i++ você incrementa depois de terminar o loop, mas ++i é incrementado diretamente antes do final do loop.

++i é mais rápido que i++ porque não retorna uma cópia antiga do valor.

Também é mais intuitivo:

 x = i++; // x contains the old value of i y = ++i; // y contains the new value of i 

Este exemplo C imprime “02” em vez do “12” que você pode esperar:

 #include  int main(){ int a = 0; printf("%d", a++); printf("%d", ++a); return 0; } 

O mesmo para o C ++ :

 #include  using namespace std; int main(){ int a = 0; cout << a++; cout << ++a; return 0; }