Pregunta

Tenemos la pregunta ¿hay alguna diferencia de rendimiento entre i ++ y ++ i en C ?

¿Cuál es la respuesta para C ++?

¿Fue útil?

Solución

[Resumen ejecutivo: Use ++ i si no tiene una razón específica para usar i ++ .]

Para C ++, la respuesta es un poco más complicada.

Si i es un tipo simple (no una instancia de una clase C ++), entonces la respuesta dada para C (" No, no hay diferencia de rendimiento ") se mantiene, ya que el compilador está generando el código.

Sin embargo, si i es una instancia de una clase de C ++, entonces i ++ y ++ i están haciendo llamadas a uno de los code> operator ++ funciones. Aquí hay un par estándar de estas funciones:

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

Dado que el compilador no genera código, sino que simplemente llama a una función operator ++ , no hay forma de optimizar la variable tmp y su constructor de copia asociado. Si el constructor de copias es costoso, esto puede tener un impacto significativo en el rendimiento.

Otros consejos

Sí. Hay.

El operador ++ puede o no definirse como una función. Para los tipos primitivos (int, double, ...) los operadores están integrados, por lo que el compilador probablemente podrá optimizar su código. Pero en el caso de un objeto que define el operador ++, las cosas son diferentes.

La función de operador ++ (int) debe crear una copia. Esto se debe a que se espera que postfix ++ devuelva un valor diferente al que posee: debe mantener su valor en una variable temporal, incrementar su valor y devolver la temperatura temporal. En el caso del operador ++ (), prefijo ++, no es necesario crear una copia: el objeto puede incrementarse y luego simplemente devolverse.

Aquí hay una ilustración del punto:

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 llama al operador ++ (int) debe crear una copia, y el compilador no puede hacer nada al respecto. Cuando se le dé la opción, use operator ++ (); De esta manera usted no guarda una copia. Puede ser significativo en el caso de muchos incrementos (¿gran bucle?) Y / u objetos grandes.

Aquí hay un punto de referencia para el caso cuando los operadores de incremento están en diferentes unidades de traducción. Compilador con g ++ 4.5.

Ignorar los problemas de estilo por ahora

// 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

Prueba

// 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 (los tiempos están en segundos) con g ++ 4.5 en una 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

Prueba

Tomemos ahora el siguiente archivo:

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

No hace nada en el incremento. Esto simula el caso cuando el incremento tiene una complejidad constante.

Resultados

Los resultados ahora varían extremadamente:

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

Conclusión

En cuanto al rendimiento

Si no necesita el valor anterior, conviértalo en un hábito para usar el pre-incremento. Sea consistente incluso con los tipos incorporados, se acostumbrará y no corre el riesgo de sufrir pérdidas de rendimiento innecesarias si alguna vez reemplaza un tipo integrado con un tipo personalizado.

Semántico-sabio

  • i ++ dice increment i, aunque estoy interesado en el valor anterior .
  • ++ i dice increment i, estoy interesado en el valor actual o increment i, no hay interés en el valor anterior . Nuevamente, te acostumbrarás a ello, incluso si no estás ahora.

Knuth.

La optimización prematura es la raíz de todo mal. Como es el pesimismo prematuro.

No es del todo correcto decir que el compilador no puede optimizar la copia de la variable temporal en el caso de postfix. Una prueba rápida con VC muestra que, al menos, puede hacerlo en ciertos casos.

En el siguiente ejemplo, el código generado es idéntico para prefijo y postfijo, por ejemplo:

#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());
}

Ya sea que haga ++ testFoo o testFoo ++, seguirá obteniendo el mismo código resultante. De hecho, sin leer el recuento del usuario, el optimizador redujo todo a una constante. Entonces esto:

for(int i=0; i<10; i++)
{
    testFoo++;
}

printf("Value: %d\n", testFoo.GetData());

Resultó en lo siguiente:

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

Entonces, si bien es cierto que la versión de postfix podría ser más lenta, es posible que el optimizador sea lo suficientemente bueno como para deshacerse de la copia temporal si no la está usando.

El Google C ++ Style Guide :

  

Preincremento y Predecremento

     

Use la forma de prefijo (++ i) de los operadores de incremento y decremento con   iteradores y otros objetos de plantilla.

     

Definición: cuando una variable se incrementa (++ i o i ++) o disminuye (--i o   i--) y el valor de la expresión no se usa, uno debe decidir   si se debe preincrementar (decrementar) o postincremento (decrementar).

     

Pros: Cuando se ignora el valor de retorno, " pre " la forma (++ i) nunca es menos   eficiente que la " publicación " forma (i ++), y suele ser más eficiente.   Esto se debe a que el post-incremento (o decremento) requiere una copia de i para   hacerse, que es el valor de la expresión. Si i es un iterador o   Otro tipo no escalar, copiar puede ser costoso. Desde los dos   los tipos de incremento se comportan igual cuando se ignora el valor, ¿por qué no?   ¿Siempre siempre pre-incremento?

     

Contras: La tradición se desarrolló, en C, de usar el incremento posterior cuando   el valor de expresión no se usa, especialmente en los bucles. Algunos encuentran   el post-incremento es más fácil de leer, ya que el " tema " (i) precede a la   " verbo " (++), al igual que en inglés.

     

Decisión: Para valores escalares simples (sin objeto) no hay razón para preferir uno   De forma y permitimos cualquiera de las dos. Para iteradores y otros tipos de plantillas, use   pre-incremento.

Me gustaría señalar un excelente post de Andrew Koenig en Code Talk muy recientemente.

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

En nuestra empresa también utilizamos la convención de ++ iter para la consistencia y el rendimiento cuando sea aplicable. Pero Andrew plantea detalles pasados ??por alto con respecto a la intención frente al rendimiento. Hay ocasiones en que queremos usar iter ++ en lugar de ++ iter.

Entonces, primero decida su intención y si pre o post no importan, vaya con pre, ya que tendrá algún beneficio de rendimiento al evitar la creación de objetos adicionales y lanzarlos.

@Ketan

  

... plantea detalles pasados ??por alto con respecto a la intención frente al rendimiento. Hay ocasiones en que queremos usar iter ++ en lugar de ++ iter.

Obviamente, la publicación y el pre-incremento tienen diferentes semánticas y estoy seguro de que todos están de acuerdo en que cuando se use el resultado, debe usar el operador apropiado. Creo que la pregunta es qué se debe hacer cuando se descarta el resultado (como en for loops). La respuesta a esta pregunta (IMHO) es que, dado que las consideraciones de rendimiento son, en el mejor de los casos, insignificantes, debe hacer lo que sea más natural. Para mí, ++ i es más natural, pero mi experiencia me dice que estoy en una minoría y que usar i ++ causará menos gastos generales de metal para la mayoría personas que leen tu código.

Después de todo, esa es la razón por la que el idioma no se llama " ++ C ". [*]

[*] Inserta una discusión obligatoria acerca de que ++ C es un nombre más lógico.

Marca: Solo quería señalar que los operadores ++ son buenos candidatos para estar en línea, y si el compilador elige hacerlo, la copia redundante se eliminará en la mayoría de los casos. (por ejemplo, tipos de POD, que suelen ser los iteradores).

Dicho esto, todavía es mejor usar ++ iter en la mayoría de los casos. :-)

La diferencia de rendimiento entre ++ i y i ++ será más evidente cuando piense en los operadores como funciones de retorno de valor y cómo se implementan. Para facilitar la comprensión de lo que está sucediendo, los siguientes ejemplos de código usarán int como si fuera una struct .

++ i incrementa la variable, luego devuelve el resultado. Esto se puede hacer en el lugar y con un tiempo mínimo de CPU, requiriendo solo una línea de código en muchos casos:

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

Pero no se puede decir lo mismo de i ++ .

El incremento posterior, i ++ , a menudo se considera que devuelve el valor original antes de el incremento. Sin embargo, una función solo puede devolver un resultado cuando está terminada . Como resultado, se hace necesario crear una copia de la variable que contiene el valor original, incrementar la variable y luego devolver la copia que contiene el valor original:

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

Cuando no hay una diferencia funcional entre el pre-incremento y el post-incremento, el compilador puede realizar una optimización tal que no exista una diferencia de rendimiento entre los dos. Sin embargo, si se trata de un tipo de datos compuesto como una struct o class , se llamará al constructor de copia en post-incremento, y no será posible realizar esto Optimización si se necesita una copia profunda. Como tal, el incremento previo generalmente es más rápido y requiere menos memoria que el incremento posterior.

  1. ++ i : más rápido que no usa el valor de retorno
  2. i ++ : más rápido utilizando el valor de retorno

Cuando no usa el valor de retorno, se garantiza que el compilador no usará temporalmente en el caso de ++ i . No se garantiza que sea más rápido, pero se garantiza que no será más lento.

Cuando usando el valor de retorno i ++ permite que el procesador presione ambos  Incrementa y el lado izquierdo en la tubería, ya que no dependen unos de otros. ++ Puedo bloquear la tubería porque el procesador no puede iniciar el lado izquierdo hasta que la operación de preincremento haya serpenteado por completo. Nuevamente, no se garantiza un bloqueo en la tubería, ya que el procesador puede encontrar otras cosas útiles para mantener.

Una de las razones por las que debería usar ++ i incluso en los tipos incorporados en los que no existe una ventaja de rendimiento es crear un buen hábito para usted mismo.

@Mark: borré mi respuesta anterior porque era un poco flip, y merecía un voto negativo solo por eso. De hecho, creo que es una buena pregunta en el sentido de que pregunta lo que piensan muchas personas.

La respuesta habitual es que ++ i es más rápido que i ++, y sin duda lo es, pero la pregunta más importante es "¿cuándo debería importarte? "

Si la fracción del tiempo de CPU empleado en incrementar los iteradores es inferior al 10%, puede que no te importe.

Si la fracción del tiempo de CPU empleado en incrementar los iteradores es superior al 10%, puede ver qué afirmaciones están haciendo esa iteración. A ver si puedes incrementar los enteros en lugar de usar iteradores. Es probable que pueda, y aunque puede ser menos deseable en cierto sentido, es muy probable que ahorre prácticamente todo el tiempo empleado en esos iteradores.

He visto un ejemplo en el que el incremento del iterador consumía más del 90% del tiempo. En ese caso, el tiempo de ejecución reducido al incremento de enteros esencialmente en esa cantidad. (es decir, mejor que 10x de aceleración)

La pregunta intencionada se refería al momento en que el resultado no se usa (eso queda claro en la pregunta para C). ¿Alguien puede solucionar esto ya que la pregunta es " wiki de la comunidad " ;?

Sobre optimizaciones prematuras, Knuth se cita a menudo. Está bien. pero Donald Knuth nunca defendería con ese horrible código que puedes ver en estos días. ¿Alguna vez has visto a = b + c entre enteros de Java (no int)? Eso equivale a 3 conversiones de boxeo / unboxing. Evitar cosas como esa es importante. Y escribir inútilmente i ++ en lugar de ++ i es el mismo error. EDITAR: como el phresnel lo pone muy bien en un comentario, esto se puede resumir como "la optimización prematura es mala, como lo es la pesimización prematura".

Incluso el hecho de que las personas estén más acostumbradas a i ++ es un desafortunado legado en C, causado por un error conceptual de K & amp; R (si sigues el argumento de la intención, esa es una conclusión lógica; y defender K & amp; R porque son K & amp; R no tiene sentido, son geniales, pero no son geniales como diseñadores de lenguaje; existen innumerables errores en el diseño de C, que van desde gets () a strcpy (), a la API strncpy () (debería haber tenido la API strlcpy () desde el día 1)).

Por cierto, soy uno de los que no estoy lo suficientemente usado para C ++ para encontrar ++ i molesto para leer. Aún así, lo uso ya que reconozco que es correcto.

@wilhelmtell

El compilador puede eludir lo temporal. Verbatim del otro hilo:

El compilador de C ++ puede eliminar temporarios basados ??en la pila, incluso si al hacerlo se modifica el comportamiento del programa. Enlace de MSDN para VC 8:

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

Es hora de proporcionar a las personas gemas de sabiduría;) - hay un truco simple para hacer que el incremento de postfijo en C ++ se comporte de manera muy similar al incremento de prefijo (lo inventé para mí mismo, pero lo vi también en el código de otras personas, así que No estoy solo.

Básicamente, el truco es usar la clase de ayuda para posponer el incremento después de la devolución, y RAII viene a rescatar

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

Invented es para algunos códigos de iteradores personalizados pesados, y reduce el tiempo de ejecución. El costo del prefijo frente a postfix es una referencia ahora, y si se trata de un operador personalizado que realiza movimientos intensos, el prefijo y el postfix me dieron el mismo tiempo de ejecución.

Ambos son tan rápidos;) Si lo desea, es el mismo cálculo para el procesador, es solo el orden en que se realiza lo que difiere.

Por ejemplo, el siguiente código:

#include <stdio.h>

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

Produce el siguiente ensamblaje:

 0x0000000100000f24 <main+0>: push   %rbp
 0x0000000100000f25 <main+1>: mov    %rsp,%rbp
 0x0000000100000f28 <main+4>: movl   <*>x0,-0x4(%rbp)
 0x0000000100000f2f <main+11>:    incl   -0x4(%rbp)
 0x0000000100000f32 <main+14>:    movl   <*>x0,-0x8(%rbp)
 0x0000000100000f39 <main+21>:    incl   -0x8(%rbp)
 0x0000000100000f3c <main+24>:    mov    <*>x0,%eax
 0x0000000100000f41 <main+29>:    leaveq 
 0x0000000100000f42 <main+30>:    retq

Usted ve que para a ++ y b ++ es un mnemónico incluido, por lo que es la misma operación;)

Cuando escribe i ++ le está diciendo al compilador que se incremente después de que finalice esta línea o bucle.

++ i es un poco diferente a i ++ . En i ++ incrementa después de terminar el ciclo pero ++ i incrementa directamente antes de que finalice el ciclo.

++ i es más rápido que i ++ porque no devuelve una copia antigua del valor.

También es más intuitivo:

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

Este ejemplo de C se imprime " 02 " en lugar de " 12 " usted podría esperar:

#include <stdio.h>

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

Lo mismo para C ++ :

#include <iostream>
using namespace std;

int main(){
    int a = 0;
    cout << a++;
    cout << ++a;
    return 0;
}
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top