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

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

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

[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 soy un tipo simple (no una instancia de una clase C ++), la respuesta dada para C (“No, no hay diferencia de rendimiento”) se cumple, ya que el comstackdor genera el código.

Sin embargo, si i es una instancia de una clase C ++, entonces i++ y ++i están realizando llamadas a una de las funciones operator++ . 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; } 

Como el comstackdor no genera código, solo llama a una función de operator++ , no hay forma de optimizar la variable tmp y su constructor de copia asociado. Si el constructor de copia es caro, esto puede tener un impacto significativo en el rendimiento.

Sí. Ahi esta.

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 comstackdor 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 de temperatura, incrementar su valor y devolver la temperatura. En el caso del operador ++ (), prefijo ++, no hay necesidad de crear una copia: el objeto puede incrementarse y luego simplemente retornar.

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 } CC::operator++(int ignored_dummy_value) { C t(*this); ++(*this); return t; // return a copy } 

Cada vez que llame al operador ++ (int) debe crear una copia y el comstackdor no puede hacer nada al respecto. Cuando se le dé la opción, use el operador ++ (); de esta manera no guardas una copia. Puede ser significativo en el caso de muchos incrementos (bucle grande?) Y / o objetos grandes.

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

Ignora los problemas de estilo por ahora

 // a.cc #include  #include  class Something { public: Something& operator++(); Something operator++(int); private: std::array 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; } 

Incremento de O (n)

Prueba

 // b.cc #include  class Something { public: Something& operator++(); Something operator++(int); private: std::array 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

Los 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  class Something { public: Something& operator++(); Something operator++(int); private: std::array 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 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. Sé coherente incluso con los tipos integrados, te acostumbrarás y no corres el riesgo de sufrir una pérdida de rendimiento innecesaria si alguna vez reemplazas un tipo integrado con un tipo personalizado.

Semántico-sabio

  • 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 estás en este momento.

Knuth.

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

No es del todo correcto decir que el comstackdor 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 hacer eso en ciertos casos.

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

 #include  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 

Ya sea que haga ++ testFoo o testFoo ++, igual obtendrá el mismo código resultante. De hecho, sin leer el recuento del usuario, el optimizador lo redujo a una constante. Así que esto:

 for(int i=0; i<10; i++) { testFoo++; } printf("Value: %d\n", testFoo.GetData()); 

Resulto 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 muy posible que el optimizador sea lo suficientemente bueno como para deshacerse de la copia temporal si no la está utilizando.

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

Preincremento y predecrement

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

Pros: cuando se ignora el valor de retorno, la forma “pre” (++ i) nunca es menos eficiente que la forma “publicar” (i ++), y con frecuencia es más eficiente. Esto se debe a que el incremento posterior (o decremento) requiere una copia de i, que es el valor de la expresión. Si soy un iterador u otro tipo no escalar, copiar 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 solo siempre se incrementa previamente?

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

Decisión: para los valores escalares simples (sin objeto) no hay ninguna razón para preferir una forma y permitimos cualquiera. Para los iteradores y otros tipos de plantillas, use el preincremento.

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

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

En nuestra empresa también utilizamos la convención de ++ iter para la coherencia y el rendimiento cuando corresponda. Pero Andrew plantea detalles descuidados con respecto a la intención frente al rendimiento. Hay momentos en los que queremos usar iter ++ en lugar de ++ iter.

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

@ Ketan

… plantea detalles descuidados con respecto a la intención frente al rendimiento. Hay momentos en los que queremos usar iter ++ en lugar de ++ iter.

Obviamente, el post y el preincremento tienen una semántica diferente y estoy seguro de que todos coinciden en que cuando se utiliza el resultado, debe usar el operador apropiado. Creo que la pregunta es qué debería hacer uno cuando se descarta el resultado (como en los bucles). La respuesta a esta 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 es más natural. Para mí ++i es más natural, pero mi experiencia me dice que soy una minoría y usar i++ causará menos sobrecarga de metal para la mayoría de las personas que leen tu código.

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

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

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

Dicho esto, todavía es mejor estilo 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 devolución de valor y cómo se implementan. Para facilitar la comprensión de lo que está sucediendo, los siguientes ejemplos de código utilizará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 de CPU mínimo, 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 incrementarse. Sin embargo, una función solo puede devolver un resultado cuando finaliza . 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 existe una diferencia funcional entre el preincremento y el incremento posterior, el comstackdor puede realizar la optimización de modo que no haya 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 el incremento posterior, y no será posible realizar esta optimización si se necesita una copia profunda. Como tal, el preincremento generalmente es más rápido y requiere menos memoria que el incremento posterior.

  1. ++ i – más rápido no usar el valor de retorno
  2. i ++ – más rápido usando el valor de retorno

Cuando no se utiliza el valor de retorno, se garantiza que el comstackdor no utilizará un elemento temporal en el caso de ++ i . No se garantiza que sea más rápido, pero se garantiza que no será más lento.

Cuando se usa el valor de retorno, i ++ permite que el procesador presione tanto el incremento como el lado izquierdo en la tubería, ya que no dependen el uno del otro. ++ puedo detener la tubería porque el procesador no puede comenzar desde el lado izquierdo hasta que la operación de preincremento ha sido todo el camino. Una vez más, no se garantiza una parada en la tubería, ya que el procesador puede encontrar otras cosas útiles para quedarse.

@ 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 qué hay en la mente de mucha gente.

La respuesta habitual es que ++ i es más rápido que i ++, y no cabe duda de que lo es, pero la pregunta más importante es “¿cuándo debería importarme?”

Si la fracción de tiempo de CPU consumida en el incremento de iteradores es inferior al 10%, entonces puede que no le importe.

Si la fracción de tiempo de CPU consumida en el incremento de iteradores es mayor al 10%, puede ver qué instrucciones están haciendo esa iteración. Vea si podría simplemente incrementar enteros en lugar de usar iteradores. Lo más probable es que puedas, y si bien puede ser, en cierto sentido, menos deseable, las posibilidades son bastante buenas de que ahorrarás esencialmente todo el tiempo que pases en esos iteradores.

He visto un ejemplo en el que el incremento de iteradores consumía más del 90% del tiempo. En ese caso, ir a incrementar incrementalmente el tiempo de ejecución por esencialmente esa cantidad. (es decir, mejor que 10 veces la aceleración)

La pregunta intencionada era acerca de cuándo el resultado no se usa (eso es claro a partir de la pregunta para C). ¿Alguien puede arreglar esto ya que la pregunta es “wiki de la comunidad”?

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

Incluso el hecho de que las personas están más acostumbradas a i ++ es un desafortunado legado de C, causado por un error conceptual de K & R (si sigues el argumento de intención, esa es una conclusión lógica, y defender K & R porque son K & R no tiene sentido, son genial, pero no son geniales como diseñadores de idiomas, existen innumerables errores en el diseño C, que van desde get () 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 se usa lo suficiente para C ++ para encontrar ++ i molesto de leer. Aún así, uso eso ya que reconozco que es correcto.

@wilhelmtell

El comstackdor puede elide el temporal. Verbatim del otro hilo:

El comstackdor de C ++ tiene permitido eliminar los temporarios basados ​​en la stack, incluso si hacerlo cambia el comportamiento del progtwig. Enlace de MSDN para VC 8:

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

Una de las razones por las que debe usar ++ i, incluso en los tipos incorporados donde no hay una ventaja de rendimiento, es crearse un buen hábito.

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

Por ejemplo, el siguiente código:

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

Produzca la siguiente asamblea:

  0x0000000100000f24 : push %rbp 0x0000000100000f25 : mov %rsp,%rbp 0x0000000100000f28 : movl $0x0,-0x4(%rbp) 0x0000000100000f2f : incl -0x4(%rbp) 0x0000000100000f32 : movl $0x0,-0x8(%rbp) 0x0000000100000f39 : incl -0x8(%rbp) 0x0000000100000f3c : mov $0x0,%eax 0x0000000100000f41 : leaveq 0x0000000100000f42 : retq 

Usted ve que para a ++ y b ++ es una mnemotecnia incl, por lo que es la misma operación;)

Es hora de proporcionar a la gente gems de sabiduría;) – hay un truco simple para hacer que el incremento de C ++ postfix se comporte más o menos como el incremento de prefijo (Inventé esto para mí, pero también lo vi en código de otras personas, entonces 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  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 contra el postfijo es una referencia ahora, y si este es un operador personalizado que realiza un movimiento pesado, el prefijo y el postfijo produjeron el mismo tiempo de ejecución para mí.

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

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

++i es más rápido que i++ porque no devuelve una copia anterior 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 C imprime “02” en lugar de “12” que podría esperar:

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

Lo mismo para C ++ :

 #include  using namespace std; int main(){ int a = 0; cout << a++; cout << ++a; return 0; }