Temos a pergunta : existe uma diferença de desempenho entre i++
e ++i
em C ?
Qual é a resposta para C ++?
Temos a pergunta : existe uma diferença de desempenho entre i++
e ++i
em C ?
Qual é a resposta para C ++?
Respostas:
[Resumo executivo: use ++i
se você não tiver um motivo específico para usá-lo i++
.]
Para C ++, a resposta é um pouco mais complicada.
Se i
for um tipo simples (não uma instância de uma classe C ++), a resposta dada para C ("Não há diferença de desempenho") é válida, pois o compilador está gerando o código.
No entanto, se i
é uma instância de uma classe C ++, então i++
e ++i
está fazendo chamadas para uma das 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 uma operator++
função, não há como otimizar a tmp
variável e seu construtor de cópias associado. Se o construtor de cópias for caro, isso poderá ter um impacto significativo no desempenho.
Sim. Há sim.
O operador ++ pode ou não ser definido como uma função. Para tipos primitivos (int, double, ...), os operadores são incorporados, portanto o compilador provavelmente poderá otimizar seu código. Mas no caso de um objeto que define o operador ++, as coisas são diferentes.
A função do operador ++ (int) deve criar uma cópia. Isso ocorre porque o postfix ++ deve retornar um valor diferente do que contém: ele deve reter seu valor em uma variável temp, incrementar seu valor e retornar a temp. No caso do operador ++ (), prefixo ++, não há necessidade de criar uma cópia: o objeto pode se incrementar e 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
}
C C::operator++(int ignored_dummy_value)
{
C t(*this);
++(*this);
return t; // return a copy
}
Toda vez que você chama o operador ++ (int), deve criar uma cópia, e o compilador não pode fazer nada sobre isso. Quando for dada a opção, 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.
C t(*this); ++(*this); return t;
Na segunda linha, você está incrementando o ponteiro this right, então como é t
atualizado se você está incrementando isso. Os valores disso já não foram copiados t
?
The operator++(int) function must create a copy.
não não é. Não há mais cópias do queoperator++()
Aqui está uma referência para o caso em que 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 <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;
}
// 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 (os tempos sã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
Vamos agora pegar 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 o incremento tem complexidade constante.
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
Se você não precisar 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 corre o risco de sofrer uma perda desnecessária de desempenho se substituir um tipo interno por um personalizado.
i++
diz increment i, I am interested in the previous value, though
.++i
diz increment i, I am interested in the current value
ou increment i, no interest in the previous value
. Mais uma vez, você se acostumará, mesmo que não esteja no momento.Otimização prematura é a raiz de todo o mal. Como é a pessimização prematura.
for (it=nearest(ray.origin); it!=end(); ++it) { if (auto i = intersect(ray, *it)) return i; }
, independentemente da estrutura da árvore real (BSP, kd, Quadtree, Octree Grid, etc.). Tal iterador seria necessário para manter um estado, por exemplo parent node
, child node
, index
e coisas assim. Ao todo, a minha posição é que, mesmo se houver apenas alguns exemplos, ...
Não é totalmente correto dizer que o compilador não pode otimizar a cópia variável temporária no caso do postfix. Um teste rápido com VC mostra que, pelo menos, 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 ++, ainda assim 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)]
Portanto, embora certamente a versão do postfix possa 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é-Predimento
Use o formato de prefixo (++ i) dos operadores de incremento e decremento 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 aumentar ou diminuir (pós-incremento) 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 decremento) exige que seja feita uma cópia de i, que é o valor da expressão. Se eu for um iterador ou outro tipo não escalar, a cópia poderá ser cara. Como os dois tipos de incremento se comportam da mesma forma quando o valor é ignorado, por que não sempre pré-incrementar?
Contras: A tradição desenvolveu, em C, o uso de pós-incremento quando o valor da expressão não é usado, especialmente em loops. Alguns acham que o pós-incremento é mais fácil de ler, pois o "assunto" (i) precede o "verbo" (++), assim como em inglês.
Decisão: para valores escalares simples (não objetos), não há razão para preferir um formulário e nós permitimos isso. 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 do ++ 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 o iter ++ em vez do ++ iter.
Portanto, primeiro decida sua intenção e, se pre ou post não interessar, vá com pre, pois ele terá algum benefício no desempenho, evitando a criação de um objeto extra e o jogando.
@Ketan
... levanta detalhes negligenciados em relação à intenção versus desempenho. Há momentos em que queremos usar o iter ++ em vez do ++ iter.
Obviamente, pós e pré-incremento têm semânticas diferentes e tenho certeza de que todos concordam que quando o resultado é usado, você deve usar o operador apropriado. Eu acho que a pergunta é o que devemos fazer quando o resultado é descartado (como em for
loops). A resposta a esta pergunta (IMHO) é que, como as considerações de desempenho são insignificantes, você deve fazer o que é mais natural. Para mim ++i
é mais natural, mas minha experiência me diz que sou uma minoria e o uso i++
causará menos sobrecarga de metal para a maioria pessoas que lê seu código.
Afinal, é por isso que o idioma não é chamado " ++C
". [*]
[*] Insira uma discussão obrigatória sobre ++C
ser um nome mais lógico.
Quando não estiver usando o valor de retorno, é garantido que o compilador não use um temporário no caso de ++ i . Não é garantido que seja mais rápido, mas que não será mais lento.
Ao usar o valor de retorno, o i ++ permite que o processador introduza o incremento e o lado esquerdo no pipeline, pois 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 se espalhado por todo o caminho. Novamente, não há garantia de uma paralisação de pipeline, pois o processador pode encontrar outras coisas úteis para se manter.
Mark: Só queria ressaltar que os operadores ++ são bons candidatos a serem incorporados e, se o compilador optar por 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 o ++ iter na maioria dos casos. :-)
A diferença de desempenho entre ++i
e i++
será mais aparente quando você considerar os operadores como funções de retorno de valor e como elas são implementadas. Para facilitar a compreensão do que está acontecendo, os seguintes exemplos de código serão usados int
como se fosse um struct
.
++i
incrementa a variável e 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 pode ser dito i++
.
Pós-incremento,, i++
geralmente é visto como retornando o valor original antes de incrementar. No entanto, uma função só pode retornar um resultado quando estiver concluída . Como resultado, torna-se necessário criar uma cópia da variável que contém o valor original, incrementar a variável e retornar a cópia mantendo 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 executar a otimização para que não haja diferença de desempenho entre os dois. No entanto, se um tipo de dados composto como a struct
ou class
estiver envolvido, o construtor de cópia será chamado no pós-incremento e não será possível executar essa otimização se uma cópia profunda for necessária. Como tal, o pré-incremento geralmente é mais rápido e requer menos memória que o pós-incremento.
@ Mark: eu apaguei minha resposta anterior, porque foi um pouco invertida, e merecia um voto negativo apenas por isso. Na verdade, acho que é uma boa pergunta, no sentido de perguntar o que está 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ê deve se importar?"
Se a fração do tempo da 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ê pode apenas incrementar números inteiros em vez de usar iteradores. As chances são de que você poderia, e embora possa ser, de certo modo, menos desejável, as chances são muito boas, você economizará essencialmente todo o tempo gasto nesses iteradores.
Eu vi um exemplo em que o incremento do iterador estava consumindo bem mais de 90% do tempo. Nesse caso, ir para o incremento inteiro reduziu o tempo de execução essencialmente por esse valor. (ou seja, melhor que 10x aceleração)
@wilhelmtell
O compilador pode excluir o temporário. Verbatim do outro segmento:
O compilador C ++ tem permissão para eliminar temporários baseados em pilha, mesmo que isso mude o comportamento do programa. Link MSDN para VC 8:
http://msdn.microsoft.com/en-us/library/ms364057(VS.80).aspx
Um dos motivos pelos quais você deve usar o ++ i, mesmo nos tipos internos em que não há vantagem de desempenho, é criar um bom hábito para si mesmo.
Ambos são tão rápidos;) Se você deseja que seja o mesmo cálculo para o processador, é apenas a ordem na qual isso é feito.
Por exemplo, o seguinte código:
#include <stdio.h>
int main()
{
int a = 0;
a++;
int b = 0;
++b;
return 0;
}
Produza o seguinte conjunto:
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. Portanto, é a mesma operação;)
A pergunta pretendida era sobre quando o resultado não é utilizado (isso fica claro na pergunta para C). Alguém pode consertar isso, pois a pergunta é "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 inteiros Java (não int)? Isso equivale a três conversões de boxe / unboxing. Evitar coisas assim é importante. E escrever inutilmente i ++ em vez de ++ i é o mesmo erro. EDIT: Como o phresnel coloca bem em um comentário, isso pode ser resumido como "otimização prematura é má, assim como pessimização prematura".
Mesmo o fato de as pessoas estarem mais acostumadas ao i ++ é um infeliz legado de C, causado por um erro conceitual da K&R (se você seguir o argumento da intenção, essa é uma conclusão lógica; defender a K&R porque é K&R não tem sentido, eles são ótimo, mas eles não são ótimos como designers de linguagem; existem inúmeros erros no design C, variando de gets () a strcpy (), até a API strncpy () (ela deveria ter a API strlcpy () desde o primeiro dia) )
Btw, eu sou um daqueles que não são usados o suficiente para C ++ para encontrar ++ eu chato de ler. Ainda assim, eu uso isso porque reconheço que está certo.
++i
mais chato do que i++
(na verdade, achei mais legal), mas o resto do seu post recebe meu total reconhecimento. Talvez adicionar um ponto de "otimização prematura é mau, como é pessimization prematuro"
strncpy
serviu a um propósito nos sistemas de arquivos que eles estavam usando na época; o nome do arquivo era um buffer de 8 caracteres e não precisava ser terminado por nulo. Você não pode culpá-los por não verem 40 anos no futuro da evolução da linguagem.
strlcpy()
foi justificada pelo fato de ainda não ter sido inventada.
Hora de fornecer às pessoas gemas de sabedoria;) - existe um truque simples para fazer com que o incremento do postfix do C ++ se comporte da mesma forma que o incremento de prefixo (inventado por mim mesmo, mas o vi também no código de outras pessoas, por isso 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 pesados de iteradores personalizados e reduz o tempo de execução. O custo do prefixo versus o postfix é uma referência agora e, se esse é um operador personalizado que faz movimentos pesados, o prefixo e o postfix produziram o mesmo tempo de execução para mim.
++i
é mais rápido do 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 dos "12" que você pode esperar:
#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;
}