¿Hay alguna diferencia de rendimiento entre i++ y ++i en C++?

14 minutos de lectura

[*]
marca harrison

Tenemos la pregunta ¿hay una diferencia de rendimiento entre i++ y ++i C ª?

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

  • Reetiqueté ya que esas dos etiquetas son la forma más fácil de encontrar preguntas de esta naturaleza. También revisé otros que no tenían etiquetas cohesivas y les di etiquetas cohesivas.

    – Jorge Stocker

    10 de septiembre de 2009 a las 15:25

  • ¿Hay alguna diferencia de rendimiento entre usar C++ y ++C?

    – nuevo123456

    26 de junio de 2011 a las 1:53

  • Artículo: ¿Es razonable usar el operador de incremento de prefijo ++it en lugar del operador de postfijo it++ para los iteradores? – viva64.com/es/b/0093

    usuario965097

    16/04/2015 a las 14:35

  • Puede depender del procesador. El PDP-11 tenía modos de direccionamiento post-incremento y pre-decremento, por lo que i++ y --i eran más eficientes que ++i y i--.

    – usuario207421

    20 oct 2021 a las 23:27

[*]
marca harrison

[Executive Summary: Use ++i if you don’t have a specific reason to use i++.]

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

Si i es un tipo simple (no una instancia de una clase de 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 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 está generando código, sino simplemente llamando a un operator++ función, no hay forma de optimizar la distancia tmp variable y su constructor de copia asociado. Si el constructor de copias es costoso, esto puede tener un impacto significativo en el rendimiento.

  • Lo que el compilador puede evitar es que la segunda copia devuelva tmp, asignando tmp en la persona que llama, a través de NRVO, como se menciona en otro comentario.

    – Blaisorblade

    15 de enero de 2009 a las 0:13

  • ¿No puede el compilador evitar esto por completo si el operador ++ está en línea?

    – Eduard-Gabriel Munteanu

    18 de febrero de 2009 a las 15:59

  • Sí, si operator++ está en línea y tmp nunca se usa, se puede eliminar a menos que el constructor o destructor del objeto tmp tenga efectos secundarios.

    – Zan Lince

    9 de septiembre de 2009 a las 1:21

  • @kriss: la diferencia entre C y C ++ es que en C tiene la garantía de que el operador estará en línea, y en ese momento un optimizador decente podrá eliminar la diferencia; en cambio, en C ++ no puede asumir la inserción, no siempre.

    – Blaisorblade

    5 de marzo de 2012 a las 10:45

  • Haría +1 SI la respuesta mencionara algo sobre las clases que contienen punteros (ya sea automático, inteligente o primitivo) a la memoria asignada dinámicamente (montón), donde el constructor de copia necesariamente realiza copias profundas. En tales casos, no hay argumento, ++i es quizás un orden de magnitud más eficiente que i++. La clave es adquirir el hábito de usar pre-incremento siempre que su algoritmo no requiera semántica de post-incremento, y entonces tendrá el hábito de escribir código que por naturaleza se presta a una mayor eficiencia, independientemente de cómo así su compilador puede optimizar.

    – etiquetador telefónico

    17 de julio de 2012 a las 18:57

¿Hay alguna diferencia de rendimiento entre i y i en
guillermo

Si. Hay.

El operador ++ puede o no estar definido como una función. Para los tipos primitivos (int, double, …) los operadores están incorporados, 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 operator++(int) debe crear una copia. Esto se debe a que se espera que postfix ++ devuelva un valor diferente al que contiene: debe mantener su valor en una variable temporal, incrementar su valor y devolver la temperatura. En el caso de operator++(), prefijo ++, no hay necesidad de crear una copia: el objeto puede incrementarse y luego simplemente regresar a sí mismo.

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 operator++(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 no guardas una copia. Puede ser significativo en el caso de muchos incrementos (¿bucle grande?) y/u objetos grandes.

  • “El operador de preincremento introduce una dependencia de datos en el código: la CPU debe esperar a que se complete la operación de incremento antes de que su valor pueda usarse en la expresión. En una CPU profundamente canalizada, esto introduce un bloqueo. No hay dependencia de datos para el operador de incremento posterior”. (Arquitectura del motor de juego (2ª edición)) Entonces, si la copia de un incremento posterior no es computacionalmente intensivo, aún puede superar el incremento previo.

    – Matías

    9 oct 2017 a las 18:18

  • En el código postfijo, ¿cómo funciona esto? C t(*this); ++(*this); return t; En la segunda línea, está incrementando este puntero a la derecha, entonces, ¿cómo t Actualícese si está incrementando esto. ¿No fueron los valores de esto ya copiados en t?

    – rasen58

    11 de noviembre de 2017 a las 3:37

  • The operator++(int) function must create a copy. no, no es. No más copias que operator++()

    – Severin Pappadeux

    7 abr 2019 a las 22:29

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

Ignora 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 son 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 la incrementación 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, acostúmbrese a usar el preincremento. Sea coherente incluso con los tipos incorporados, se acostumbrará y no correrá el riesgo de sufrir una pérdida de rendimiento innecesaria si alguna vez reemplaza un tipo incorporado con un tipo personalizado.

Semánticamente

  • i++ dice increment i, I am interested in the previous value, though.
  • ++i dice increment i, I am interested in the current value o increment i, no interest in the previous value. Nuevamente, te acostumbrarás, incluso si no lo estás ahora.

Knuth.

La optimización prematura es la fuente de todos los males. Como lo es la pesimización prematura.

  • Prueba interesante. Ahora, casi dos años y medio después, gcc 4.9 y Clang 3.4 muestran una tendencia similar. Clang es un poco más rápido con ambos, pero la disparidad entre pre y postfix es peor que gcc.

    – masticar calcetines

    14 de agosto de 2014 a las 15:06

  • Lo que realmente me gustaría ver es un ejemplo del mundo real donde ++i / i++ marque la diferencia. Por ejemplo, ¿hace alguna diferencia en alguno de los iteradores estándar?

    –Jakob Schou Jensen

    4 de junio de 2015 a las 6:58

  • @JakobSchouJensen: Estos tenían la intención de ser ejemplos del mundo real. Considere una aplicación grande, con estructuras de árbol complejas (p. ej., kd-trees, quad-trees) o contenedores grandes utilizados en plantillas de expresión (para maximizar el rendimiento de datos en el hardware SIMD). Si hace una diferencia allí, no estoy realmente seguro de por qué uno recurriría al incremento posterior para casos específicos si eso no es necesario desde el punto de vista semántico.

    – Sebastián Mach

    5 de junio de 2015 a las 8:31


  • @phresnel: No creo que operator++ esté en su plantilla de expresión diaria. ¿Tiene un ejemplo real de esto? El uso típico de operator++ es en enteros e iteradores. Ahí es donde creo que sería interesante saber si hay alguna diferencia (no hay diferencia en los números enteros, por supuesto, sino en los iteradores).

    –Jakob Schou Jensen

    6 jun 2015 a las 15:35

  • @JakobSchouJensen: No hay un ejemplo comercial real, pero sí algunas aplicaciones de procesamiento de números en las que cuenta cosas. Wrt iteradores, considere un trazador de rayos que está escrito en estilo idiomático C ++, y tiene un iterador para el recorrido primero en profundidad, de modo que for (it=nearest(ray.origin); it!=end(); ++it) { if (auto i = intersect(ray, *it)) return i; }, no importa la estructura del árbol real (BSP, kd, Quadtree, Octree Grid, etc.). Tal iterador necesitaría mantener algún estado, por ejemplo parent node, child node, index Y cosas como esa. Con todo, mi postura es, incluso si existen pocos ejemplos, …

    – Sebastián Mach

    7 junio 2015 a las 20:16

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

En el siguiente ejemplo, el código generado es idéntico para el prefijo y el sufijo, 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++, obtendrá el mismo código resultante. De hecho, sin leer el conteo del usuario, el optimizador redujo todo a una constante. Así que 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 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.

1647569358 606 ¿Hay alguna diferencia de rendimiento entre i y i en
martjno

los Guía de estilo de Google C++ dice:

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 se decrementa (–i o i–) y no se utiliza el valor de la expresión, se debe decidir si se preincrementa (decrementa) o se posincrementa (decrementa).

Ventajas: Cuando se ignora el valor de retorno, la forma “pre” (++i) nunca es menos eficiente que la forma “post” (i++) y suele ser más eficiente. Esto se debe a que el posincremento (o decremento) requiere que se haga una copia de i, que es el valor de la expresión. Si i es un iterador u otro tipo no escalar, copiar i podría ser costoso. Dado que los dos tipos de incremento se comportan de la misma manera cuando se ignora el valor, ¿por qué no realizar siempre un incremento previo?

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

Decisión: Para valores escalares simples (no objetos) no hay razón para preferir una forma y permitimos cualquiera de las dos. Para iteradores y otros tipos de plantillas, use preincremento.

  • “Decisión: para valores escalares simples (no objetos) no hay razón para preferir una forma y permitimos cualquiera de las dos. Para iteradores y otros tipos de plantillas, use el incremento previo”.

    – Nosredna

    28 de noviembre de 2009 a las 17:41

  • Eh,…, y que es ese algo?

    – Sebastián Mach

    15 de noviembre de 2014 a las 10:06

  • El enlace mencionado en la respuesta está actualmente roto.

    – Karol

    12 de julio de 2019 a las 7:12

Me gustaría señalar una excelente publicación de Andrew Koenig en Code Talk muy recientemente.

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

En nuestra empresa también usamos la convención de ++iter para mantener la coherencia y el rendimiento cuando corresponde. Pero Andrew plantea detalles pasados ​​por alto con respecto a la intención frente al rendimiento. Hay ocasiones en las que queremos usar iter++ en lugar de ++iter.

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

  • “Decisión: para valores escalares simples (no objetos) no hay razón para preferir una forma y permitimos cualquiera de las dos. Para iteradores y otros tipos de plantillas, use el incremento previo”.

    – Nosredna

    28 de noviembre de 2009 a las 17:41

  • Eh,…, y que es ese algo?

    – Sebastián Mach

    15 de noviembre de 2014 a las 10:06

  • El enlace mencionado en la respuesta está actualmente roto.

    – Karol

    12 de julio de 2019 a las 7:12

@Ketan

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

Obviamente, la publicación y el preincremento tienen una semántica diferente y estoy seguro de que todos están de acuerdo en que cuando se usa el resultado, debe usar el operador apropiado. Creo que la pregunta es qué se debe hacer cuando se descarta el resultado (como en for bucles). la respuesta a esta La pregunta (en mi humilde opinión) es que, dado que las consideraciones de rendimiento son insignificantes en el mejor de los casos, debe hacer lo que sea más natural. Para mí ++i es mas natural pero mi experiencia me dice que soy minoria y uso i++ causará menos sobrecarga de metal para más personas que leen su código.

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

[*] Insertar discusión obligatoria sobre ++C siendo un nombre más lógico.

  • @Motti: (bromeando) El nombre de C++ es lógico si recuerda que Bjarne Stroustrup C++ lo codificó inicialmente como un precompilador que genera un programa en C. Por lo tanto, C++ devolvió un valor antiguo de C. O puede ser para resaltar que C ++ tiene algunas fallas conceptuales desde el principio.

    – kriss

    31 de marzo de 2010 a las 11:37

¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad