que - ¿Hay una diferencia de rendimiento entre i++ y++ i en C++?
preincremento y postincremento java (18)
- ++ i - más rápido sin usar el valor de retorno
- i ++ - más rápido usando el valor de retorno
Cuando no se usa el valor de retorno, se garantiza que el compilador no use un temporal en el caso de ++ i . No se garantiza que sea más rápido, pero se garantiza que no sea más lento.
Cuando se usa el valor de retorno, i ++ permite al procesador empujar tanto el incremento como el lado izquierdo hacia 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.
Tenemos la pregunta : ¿hay una diferencia de rendimiento entre i++
y ++i
en C ?
¿Cuál es la respuesta para C ++?
@Ketan
... plantea detalles exagerados con respecto a la intención frente al rendimiento. Hay ocasiones en las que queremos usar iter ++ en lugar de ++ iter.
Obviamente, post y pre-incremento tienen diferentes semánticas 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 los bucles). La respuesta a esta pregunta (IMHO) es que, como 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 para la mayoría de las personas que leen su código.
Después de todo, esa es la razón por la que el lenguaje no se llama " ++C
". [*]
[*] Insertar discusión obligatoria sobre ++C
siendo un nombre más lógico.
@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é piensan las 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 le importe.
Si la fracción del tiempo de CPU empleado en incrementar los iteradores es superior al 10%, puede ver qué instrucciones 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 en cierto sentido menos deseable, es muy probable que ahorre esencialmente 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)
@wilhelmtell
El compilador puede esquivar lo temporal. Verbatim del otro hilo:
Al compilador de C ++ se le permite eliminar los temporales basados en la pila, incluso si al hacerlo cambia el comportamiento del programa. Enlace MSDN para VC 8:
http://msdn.microsoft.com/en-us/library/ms364057(VS.80).aspx
Ambos son tan rápidos;) Si lo desea, es el mismo cálculo para el procesador, lo que difiere es el orden en que se realiza.
Por ejemplo, el siguiente código:
#include <stdio.h>
int main()
{
int a = 0;
a++;
int b = 0;
++b;
return 0;
}
Produzca el siguiente montaje:
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
Usted ve que para a ++ y b ++ es un mnemónico incluido, por lo que es la misma operación;)
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.
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 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 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 de usar 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.
En sentido semántico
-
i++
diceincrement i, I am interested in the previous value, though
. -
++i
diceincrement i, I am interested in the current value
oincrement i, no interest in the previous value
. Nuevamente, te acostumbrarás a ello, incluso si no estás ahora.
Knuth.
La optimización prematura es la fuente de todos los males. Como es pesimismo prematuro.
Cuando escribes i++
le estás diciendo al compilador que se incremente después de que termine esta línea o bucle.
++i
es un poco diferente a i++
. En i++
, aumenta después de terminar el ciclo, pero ++i
aumenta directamente antes de que finalice el ciclo.
Es hora de proporcionar a las personas gemas de sabiduría;): hay un simple truco para hacer que el incremento de C ++ postfix se comporte de forma muy parecida al incremento de prefijo (lo inventé para mí, pero lo vi también en el código de otras personas, así que no lo hago). 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;
}
Inventado es para un código pesado de iteradores personalizados, y reduce el tiempo de ejecución. El costo de prefix vs postfix es una referencia ahora, y si se trata de un operador personalizado que realiza movimientos intensos, prefijo y postfix me dieron el mismo tiempo de ejecución.
La 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 disminuye (--i o i--) y no se usa el valor de la expresión, se debe decidir si se debe aumentar (disminuir) o postincrementar (disminuir).
Pros: 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 post-incremento (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 podría ser costoso. Dado que los dos tipos de incremento se comportan igual cuando se ignora el valor, ¿por qué no siempre pre-incremento?
Contras: La tradición se desarrolló, en C, de usar el incremento posterior cuando no se usa el valor de la expresión, especialmente en los bucles for. Algunos encuentran que 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 valores escalares simples (sin objeto) no hay razón para preferir una forma y permitimos cualquiera. Para los iteradores y otros tipos de plantillas, use pre-incremento.
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 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 aumentar. Sin embargo, una función solo puede devolver un resultado cuando se termina . Como resultado, es 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 diferencia funcional entre pre-incremento y post-incremento, el compilador puede realizar la optimización de tal manera 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 post-incremento, y no será posible realizar esta optimización si se necesita una copia profunda. Como tal, el pre-incremento generalmente es más rápido y requiere menos memoria que el post-incremento.
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 arreglar 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 eso el 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 Phresnel bien lo pone en un comentario, esto puede resumirse como "la optimización prematura es mala, como lo es la pesimación 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 la intención, esa es una conclusión lógica; y defender K&R porque son K&R no tiene sentido, genial, pero no son tan buenos 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 he usado lo suficiente como para que C ++ pueda encontrar ++ i molesto para leer. Aún así, lo uso porque reconozco que es correcto.
Mark: 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, los tipos de POD, que suelen ser los iteradores).
Dicho esto, todavía es mejor el estilo para usar ++ iter en la mayoría de los casos. :-)
Me gustaría señalar un excelente post 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 utilizamos la convención de ++ iter para la consistencia y el rendimiento cuando sea aplicable. Pero Andrew plantea detalles sobre la intención frente al rendimiento. Hay ocasiones en las que queremos usar iter ++ en lugar de ++ iter.
Por lo tanto, primero decida su intención y si el pre o post no importa, vaya con el pre ya que tendrá algún beneficio de rendimiento al evitar la creación de un objeto adicional y lanzarlo.
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 hacer eso 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 hagas ++ testFoo o testFoo ++, seguirás obteniendo el mismo código resultante. De hecho, sin leer la cuenta del usuario, el optimizador hizo que todo se redujera 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 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á utilizando.
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 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 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 devolverlo.
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.
Y la razón por la 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.
[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 ++), 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 una de las funciones de 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;
}
Dado que el compilador no genera código, sino que simplemente 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 copias es costoso, esto puede tener un impacto significativo en el rendimiento.
++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 imprime "02" en lugar del "12" que podría esperar:
#include <stdio.h>
int main(){
int a = 0;
printf("%d", a++);
printf("%d", ++a);
return 0;
}
#include <iostream>
using namespace std;
int main(){
int a = 0;
cout << a++;
cout << ++a;
return 0;
}