linguagem - diferença entre c# c++ ec




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

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

Qual a resposta para o C ++?


  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 para ser mais rápido, mas garantido para não ser 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 no pipeline não é garantida, já que o processador pode encontrar outras coisas úteis.


@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 eu estou em minoria e usar o i++ causará menos sobrecarga de metal para a maioria das pessoas que lêem seu código.

Afinal, essa é a razão pela qual a linguagem não é chamada " ++C ". [*]

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



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 elas são implementadas. 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++ .

O pós-incremento, i++ , é visto frequentemente como retornando o valor original antes de incrementar. 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 contendo o valor original, incrementar a variável e 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 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 memória que o pós-incremento.


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 <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 um ++ e b ++ é um mnemônico incl, então é a mesma operação;)


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 <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;
}

O (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 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 <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 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 substituir 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.


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. :-)


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, pode fazer isso em certos casos.

No exemplo a seguir, o código gerado é idêntico para o prefixo e o 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 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.


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 você incrementa diretamente antes do final do loop.


Sim. Há sim.

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.

O operador ++ (int) função 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 objeto 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
}

C C::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 a respeito. 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 objetos grandes.


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


[Resumo 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 classe 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 classe 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 função 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.







pre-increment