Pergunta

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

Qual é a resposta para C++?

Foi útil?

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++ 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.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.

  1. ++eu - mais rápido não use o valor de retorno
  2. 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;
}

O mesmo para C++:

#include <iostream>
using namespace std;

int main(){
    int a = 0;
    cout << a++;
    cout << ++a;
    return 0;
}
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top