Tenemos la pregunta ¿hay una diferencia de rendimiento entre i++
y ++i
en C ?
¿Cuál es la respuesta para C ++?
Tenemos la pregunta ¿hay una diferencia de rendimiento entre i++
y ++i
en C ?
¿Cuál es la respuesta para C ++?
Respuestas:
[Resumen ejecutivo: utilícelo ++i
si no tiene un motivo específico para utilizarlo 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") es válida, ya que el compilador está generando el código.
Sin embargo, si i
es una instancia de una clase C ++, entonces i++
y ++i
está haciendo llamadas a una de las 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;
}
Como el compilador no genera código, sino que solo llama a una operator++
función, no hay forma de optimizar la tmp
variable y su constructor de copia asociado. Si el constructor de copias es costoso, esto puede tener un impacto significativo en el rendimiento.
Si. 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 contiene: debe mantener su valor en una variable temporal, 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 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 no guarda una copia. Puede ser significativo en el caso de muchos incrementos (¿bucle grande?) Y / u objetos grandes.
C t(*this); ++(*this); return t;
En la segunda línea, estás incrementando este puntero correctamente, entonces, ¿cómo t
se actualiza si estás incrementando esto? ¿No se copiaron ya los valores de esto t
?
The operator++(int) function must create a copy.
no, no es. No más copias queoperator++()
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;
}
// 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 (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
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.
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
Si no necesita el valor anterior, acostúmbrese a usar pre-incremento. 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 integrado con un tipo personalizado.
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
. Una vez más, te acostumbrarás, incluso si no lo estás ahora.La optimización prematura es la fuente de todos los males. Como es el pesimismo prematuro.
for (it=nearest(ray.origin); it!=end(); ++it) { if (auto i = intersect(ray, *it)) return i; }
, no importa la estructura de árbol real (BSP, kd, Quadtree, Octree Grid, etc.). Tal iterador necesitaría mantener un estado, por ejemplo parent node
, child node
, index
y cosas por el estilo. En general, mi postura es, incluso si existen pocos ejemplos, ...
No es del todo correcto decir que el compilador no puede optimizar la copia 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 el prefijo y el postfix, 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. 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á usando.
La Guía de estilo de Google C ++ dice:
Preincremento y Predecremento
Utilice 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 preincremento (decremento) o postincremento (decremento).
Pros: cuando se ignora el valor de retorno, la forma "pre" (++ i) nunca es menos eficiente que la forma "post" (i ++) y, a menudo, es más eficiente. Esto se debe a que el incremento posterior (o decremento) requiere que se haga una copia de i, que es el valor de la expresión. Si soy un iterador u otro tipo no escalar, copiarlo 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 siempre pre-incrementar?
Contras: La tradición se desarrolló, en C, de usar post-incremento cuando no se usa el valor de expresión, especialmente en for loops. Algunos encuentran que el incremento posterior es más fácil de leer, ya que el "asunto" (i) precede al "verbo" (++), al igual que en inglés.
Decisión: Para valores escalares simples (no objeto) no hay razón para preferir una forma y permitimos cualquiera. Para iteradores y otros tipos de plantillas, use pre-incremento.
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 utilizamos la convención de ++ iter para obtener consistencia y rendimiento cuando corresponda. Pero Andrew plantea detalles pasados por alto con respecto a la intención frente al rendimiento. Hay momentos en los que queremos usar iter ++ en lugar de ++ iter.
Por lo tanto, primero decida su intención y si pre o post no importa, vaya con pre, ya que tendrá algún beneficio de rendimiento al evitar la creación de objetos adicionales y arrojarlos.
@Ketan
... plantea detalles pasados por alto con respecto a la intención frente al rendimiento. Hay momentos en los que queremos usar iter ++ en lugar de ++ iter.
Obviamente, post y pre-incremento tienen semánticas diferentes 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é debería hacer uno cuando se descarta el resultado (como en los for
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 minoritario y que el uso i++
causará menos gastos generales de metal para la mayoría personas que leen su código.
Después de todo, esa es la razón por la cual el idioma no se llama " ++C
". [*]
[*] Insertar discusión obligatoria sobre ++C
ser un nombre más lógico.
Cuando no se utiliza el valor de retorno, se garantiza que el compilador no utilizará 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 que el procesador empuje tanto el incremento como el lado izquierdo hacia la tubería, ya que no dependen el uno del otro. ++ Puedo detener la canalización porque el procesador no puede iniciar el lado izquierdo hasta que la operación de preincremento haya recorrido todo el camino. Una vez más, no se garantiza un bloqueo de la tubería, ya que el procesador puede encontrar otras cosas útiles para quedarse.
Mark: Solo quería señalar que los operadores ++ son buenos candidatos para estar en línea, y si el compilador decide 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 se usarán int
como si fuera un 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 i++
.
El incremento posterior i++
, a menudo se considera que devuelve el valor original antes de incrementar. 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 compuestos como a struct
o class
, se llamará al constructor de la copia en el post-incremento y no será posible realizar esta 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.
@ Mark: eliminé 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é piensa mucha gente.
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 importarle?"
Si la fracción del tiempo de CPU invertido en los iteradores incrementales es inferior al 10%, es posible que no le importe.
Si la fracción del tiempo de CPU invertido en incrementos de iteradores es mayor al 10%, puede ver qué declaraciones están haciendo esa iteración. Vea si podría simplemente incrementar los enteros en lugar de usar iteradores. Lo más probable es que pueda, y aunque en cierto sentido puede ser menos deseable, es muy probable que ahorre esencialmente todo el tiempo que pase en esos iteradores.
He visto un ejemplo en el que el incremento de iterador consumía más del 90% del tiempo. En ese caso, ir al incremento de números enteros redujo esencialmente el tiempo de ejecución en esa cantidad. (es decir, mejor que 10x de aceleración)
@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 cambia el comportamiento del programa. Enlace MSDN para VC 8:
http://msdn.microsoft.com/en-us/library/ms364057(VS.80).aspx
Y la razón por la que deberías usar ++ i incluso en tipos integrados donde no hay ventaja de rendimiento es crear un buen hábito para ti.
Ambos son tan rápidos;) Si lo desea, es el mismo cálculo para el procesador, es solo el orden en el que se hace lo que difiere.
Por ejemplo, el siguiente código:
#include <stdio.h>
int main()
{
int a = 0;
a++;
int b = 0;
++b;
return 0;
}
Produzca el siguiente ensamblaje:
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
Verá que para a ++ y b ++ es un mnemónico incluido, por lo que es la misma operación;)
La pregunta que se pretendía era cuándo no se utiliza el resultado (eso queda claro 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. Así es. pero Donald Knuth nunca defendería con ese 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 como esas 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&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, son genial, pero no son excelentes 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 acostumbrado a C ++ para encontrar ++ i molesto para leer. Aún así, lo uso ya que reconozco que es correcto.
++i
más molesto que i++
(de hecho, lo encontré más fresco), pero el resto de tu publicación recibe mi reconocimiento completo. Tal vez agregue un punto "la optimización prematura es malvada, como lo es la pesimismo prematura"
strncpy
cumplió un propósito en los sistemas de archivos que estaban usando en ese momento; el nombre del archivo era un búfer de 8 caracteres y no tenía que ser anulado. No se les puede culpar por no ver 40 años en el futuro de la evolución del lenguaje.
strlcpy()
fue justificada por el hecho de que aún no se había inventado.
Es hora de proporcionar gemas de sabiduría a las personas;): hay un simple truco para hacer que el incremento de postfix de C ++ se comporte casi igual que el incremento de prefijo (lo inventé para mí, pero también lo vi en el código de otras personas, así que no estoy solo).
Básicamente, el truco es usar la clase auxiliar para posponer el incremento después del regreso, 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 algunos códigos de iteradores personalizados pesados, y reduce el tiempo de ejecución. El costo del prefijo vs postfix es una referencia ahora, y si este es un operador personalizado que se mueve mucho, el prefijo y el postfix me dieron el mismo tiempo de ejecución.
++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;
}