Existe uma diferença de desempenho entre i++ e ++i em C++?
-
09-06-2019 - |
Pergunta
Nós temos a pergunta existe uma diferença de desempenho entre i++
e ++i
em C?
Qual é a resposta para C++?
Solução
[Sumário executivo:Usar ++i
se você não tiver um motivo específico para usar i++
.]
Para C++, a resposta é um pouco mais complicada.
Se i
é um tipo simples (não uma instância de uma classe C++), então a resposta dada para C ("Não, não há diferença de desempenho") é válido, já que o compilador está gerando o código.
No entanto, se i
é uma instância de uma classe C++, então i++
e ++i
estão fazendo ligações para um dos operator++
funções.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 um operator++
função, não há como otimizar o tmp
variável e seu construtor de cópia associado.Se o construtor de cópia for caro, isso poderá ter um impacto significativo no desempenho.
Outras dicas
Sim.Há.
O operador ++ pode ou não ser definido como uma função.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 objeto que define o operador ++ as coisas são diferentes.
A função operador++(int) deve criar uma cópia.Isso ocorre porque espera-se que o postfix ++ retorne um valor diferente daquele que contém:ele deve manter seu valor em uma variável temporária, incrementar seu valor e retornar a temperatura.No caso de operador++(), prefixo ++, não há necessidade de criar uma cópia:o objeto pode se incrementar e simplesmente retornar.
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
}
C C::operator++(int ignored_dummy_value)
{
C t(*this);
++(*this);
return t; // return a copy
}
Cada vez que você chama o operador++(int) você deve criar uma cópia e o compilador não pode fazer nada a respeito.Quando puder escolher, use operator++();dessa forma você não salva uma cópia.Pode ser significativo no caso de muitos incrementos (loop grande?) E/ou objetos grandes.
Aqui está uma referência para o caso em que os operadores de incremento estão em unidades de tradução diferentes.Compilador com g++ 4.5.
Ignore os problemas de estilo por enquanto
// a.cc
#include <ctime>
#include <array>
class Something {
public:
Something& operator++();
Something operator++(int);
private:
std::array<int,PACKET_SIZE> 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;
}
Sobre(n) incremento
Teste
// b.cc
#include <array>
class Something {
public:
Something& operator++();
Something operator++(int);
private:
std::array<int,PACKET_SIZE> 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 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
Tomemos agora o seguinte arquivo:
// c.cc
#include <array>
class Something {
public:
Something& operator++();
Something operator++(int);
private:
std::array<int,PACKET_SIZE> 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 em que a incrementação tem complexidade constante.
Resultados
Os resultados agora variam extremamente:
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
Em termos de desempenho
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á e não correrá o risco de sofrer perda desnecessária de desempenho se substituir um tipo interno por um tipo personalizado.
Em termos semânticos
i++
dizincrement i, I am interested in the previous value, though
.++i
dizincrement i, I am interested in the current value
ouincrement i, no interest in the previous value
.Novamente, você se acostumará, mesmo que não esteja agora.
Knuth.
Otimização prematura é a raiz de todo o mal.Assim 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 VC mostra que pelo menos ele pode fazer isso em certos casos.
No exemplo a seguir, o código gerado é idêntico para prefixo e postfix, por exemplo:
#include <stdio.h>
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<count; i++)
{
testFoo++;
}
printf("Value: %d\n", testFoo.GetData());
}
Quer você faça ++testFoo ou testFoo++, você ainda obterá o mesmo código resultante.Na verdade, sem ler a contagem do usuário, o otimizador reduziu tudo 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)]
Portanto, embora certamente a versão postfix possa ser mais lenta, pode muito bem 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é-decremento
Use a forma de prefixo (++ i) dos operadores de incremento e decréscimo com iteradores e outros objetos 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, é preciso decidir se deve prender (decrementar) ou pós-incremento (decremento).
Prós: Quando o valor de retorno é ignorado, o formulário "pré" (++ i) nunca é menos eficiente que o formulário "post" (i ++) e geralmente é mais eficiente.Isso ocorre porque o pós-incremento (ou decréscimo) requer uma cópia de I a ser feita, que é o valor da expressão.Se eu for um iterador ou outro tipo não escalar, copiar, poderia ser caro.Como os dois tipos de incremento se comportam da mesma forma quando o valor é ignorado, por que não sempre o pré-incremento?
Contras: A tradição se desenvolveu, em C, de usar pós-incremento quando o valor da expressão não é usado, especialmente para loops.Alguns acham mais fácil de ler o pós-incremento, já que o "sujeito" (i) precede o "verbo" (++), assim como em inglês.
Decisão: Para valores escalares simples (não objeto), não há razão para preferir uma forma e também permitimos.Para iteradores e outros tipos de modelo, use pré-incremento.
Gostaria de destacar um excelente post de Andrew Koenig no Code Talk muito recentemente.
http://dobbscodetalk.com/index.php?option=com_myblog&show=Efficiency-versus-intent.html&Itemid=29
Em nossa empresa também usamos a convenção ++iter para consistência e desempenho quando aplicável.Mas Andrew levanta detalhes negligenciados em relação à intenção versus desempenho.Há momentos em que queremos usar iter++ em vez de ++iter.
Portanto, primeiro decida sua intenção e se pré ou pós não importa, escolha pré, pois terá algum benefício de desempenho, evitando a criação de objetos extras e seu lançamento.
@Ketan
... levanta detalhes negligenciados em relação à intenção versus desempenho.Há momentos em que queremos usar iter++ em vez de ++iter.
Obviamente o pós e o pré-incremento têm semânticas diferentes e tenho certeza que todos concordam que quando o resultado for usado você deve usar o operador apropriado.Penso que a questão é o que se deve fazer quando o resultado é descartado (como em for
rotações).A resposta para esse A questão (IMHO) é que, como as considerações de desempenho são, na melhor das hipóteses, insignificantes, você deve fazer o que é mais natural.Para mim ++i
é mais natural, mas minha experiência me diz que estou em minoria e uso i++
causará menos sobrecarga de metal para maioria pessoas lendo seu código.
Afinal essa é a razão pela qual a linguagem não é chamada "++C
".[*]
[*] Inserir discussão obrigatória sobre ++C
sendo um nome mais lógico.
Marca:Queria apenas salientar que os operadores ++ são bons candidatos para serem incorporados e, se o compilador decidir fazê-lo, a cópia redundante será eliminada na maioria dos casos.(por exemplo.Tipos de POD, que geralmente são os iteradores.)
Dito isto, ainda é melhor usar ++iter na maioria dos casos.:-)
A diferença de desempenho entre ++i
e i++
ficará mais aparente quando você pensar nos operadores como funções que retornam 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 um struct
.
++i
incrementa a variável, então retorna o resultado.Isso pode ser feito no local e com tempo mínimo de CPU, exigindo apenas uma linha de código em muitos casos:
int& int::operator++() {
return *this += 1;
}
Mas o mesmo não se pode dizer de i++
.
Pós-incremento, i++
, geralmente é visto como retornando o valor original antes incrementando.No entanto, uma função só pode retornar um resultado quando terminar.Como resultado, torna-se necessário criar uma cópia da variável que contém o valor original, incrementar a variável e depois 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 o pré-incremento e o 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 um struct
ou class
estiver envolvido, o construtor de cópia será chamado no pós-incremento e não será possível realizar esta otimização se uma cópia profunda for necessária.Como tal, o pré-incremento geralmente é mais rápido e requer menos memória do que o pós-incremento.
- ++eu - mais rápido não use o valor de retorno
- eu++ - mais rápido usando o valor de retorno
Quando não use o valor de retorno, o compilador tem a garantia de não usar um temporário no caso de ++eu.Não é garantido que seja mais rápido, mas é garantido que não será mais lento.
Quando usando o valor de retorno eu++ Permite que o processador empurre o incremento e o lado esquerdo para o pipeline, pois eles não dependem um do outro.++i pode travar 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.Novamente, uma parada no pipeline não é garantida, pois o processador pode encontrar outras coisas úteis para se manter.
A razão pela qual você deve usar ++i mesmo em tipos integrados onde não há vantagem de desempenho é criar um bom hábito para você.
@Marca:Excluí minha resposta anterior porque era um pouco invertida e merecia um voto negativo apenas por isso.Na verdade, acho que é uma boa pergunta, no sentido de que pergunta o que se passa na cabeça de muitas pessoas.
A resposta usual é que ++i é mais rápido que i++, e sem dúvida é, mas a grande questão é "quando você deveria se importar?"
Se a fração do tempo de CPU gasto no incremento de iteradores for inferior a 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 números inteiros em vez de usar iteradores.Provavelmente, você poderia, e embora possa ser, em certo sentido, menos desejável, as chances são muito boas de que você economize essencialmente todo o tempo gasto nesses iteradores.
Eu vi um exemplo em que o incremento do iterador consumia bem mais de 90% do tempo.Nesse caso, passar para o incremento de números inteiros reduziu o tempo de execução essencialmente nessa quantidade.(ou seja,melhor que aceleração de 10x)
A pergunta pretendida era sobre quando o resultado não é utilizado (isso fica 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.Isso mesmo.mas Donald Knuth nunca defenderia com isso o código horrível que você pode ver hoje em dia.Já viu a = b + c entre inteiros Java (não int)?Isso equivale a 3 conversões de boxing/unboxing.Evitar coisas assim é importante.E escrever inutilmente i++ em vez de ++i é o mesmo erro.EDITAR:Como Phresnel bem coloca em um comentário, isso pode ser resumido como "a otimização prematura é má, assim como a pessimização prematura".
Até mesmo o fato de as pessoas estarem mais acostumadas com i++ é um legado infeliz do C, causado por um erro conceitual da K&R (se você seguir o argumento da intenção, essa é uma conclusão lógica;e defender K&R porque são K&R não tem sentido, eles são ótimos, mas não são ótimos como designers de linguagem;existem inúmeros erros no design C, variando de get() a strcpy(), até a API strncpy() (deveria ter a API strlcpy() desde o primeiro dia)).
Aliás, sou um daqueles que não está acostumado o suficiente com C++ para achar ++i chato de ler.Ainda assim, eu uso isso porque reconheço que está certo.
@wilhelmtell
O compilador pode eliminar o temporário.Literalmente do outro tópico:
O compilador C++ pode eliminar temporários baseados em pilha, mesmo que isso altere o comportamento do programa.Link MSDN para VC 8:
http://msdn.microsoft.com/en-us/library/ms364057(VS.80).aspx
É hora de fornecer às pessoas joias de sabedoria;) - existe um truque simples para fazer com que o incremento de postfix C++ se comporte praticamente da mesma forma que o incremento de prefixo (inventei isso para mim, mas também o vi no código de outras pessoas, então não estou sozinho).
Basicamente, o truque é usar a classe auxiliar para adiar o incremento após o retorno, e o RAII vem para resgatar
#include <iostream>
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 alguns códigos de iteradores personalizados pesados e reduz o tempo de execução.O custo do prefixo versus postfix é uma referência agora, e se este for um operador personalizado que faz movimentos pesados, o prefixo e o postfix produziram o mesmo tempo de execução para mim.
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 seguinte código:
#include <stdio.h>
int main()
{
int a = 0;
a++;
int b = 0;
++b;
return 0;
}
Produza a seguinte montagem:
0x0000000100000f24 <main+0>: push %rbp 0x0000000100000f25 <main+1>: mov %rsp,%rbp 0x0000000100000f28 <main+4>: movl $0x0,-0x4(%rbp) 0x0000000100000f2f <main+11>: incl -0x4(%rbp) 0x0000000100000f32 <main+14>: movl $0x0,-0x8(%rbp) 0x0000000100000f39 <main+21>: incl -0x8(%rbp) 0x0000000100000f3c <main+24>: mov $0x0,%eax 0x0000000100000f41 <main+29>: leaveq 0x0000000100000f42 <main+30>: retq
Você vê que para a++ e b++ é um mnemônico incl, então é a mesma operação;)
Quando você escreve i++
você está dizendo ao compilador para incrementar depois de terminar esta linha ou loop.
++i
é um pouco diferente de i++
.Em i++
você incrementa depois de terminar o loop, mas ++i
você incrementa diretamente antes do término 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 de "12" que você esperaria:
#include <stdio.h>
int main(){
int a = 0;
printf("%d", a++);
printf("%d", ++a);
return 0;
}
#include <iostream>
using namespace std;
int main(){
int a = 0;
cout << a++;
cout << ++a;
return 0;
}