¿Preincrementar o postincrementar?

Hace mucho que quería escribir un post tan inútil como este. La duda que siempre me pasaba por la cabeza era la siguiente: ¿Será exactamente el mismo código ensamblador el que genera el compilador cuando utilizamos el preincremento o el postincremento (sin utilizar el valor de retorno)? Tenía la seguridad de que así debía ser (una optimización tan básica no podía ser dejada de lado), pero me faltaban las pruebas (en ensamblador) para verificarlo.

Recordando un poco, el preincremento
int a = 0;
int b = ++a;
hace a=1 y b=1, mientras que el postincremento
int a = 0;
int b = a++;
hace a=1 y b=0, esto significa que b obtuvo el valor de a anterior al incremento.

Aquí estamos utilizando el valor de retorno del operador incremento. El código ensamblador es distinto para cada caso. Vamos a echarle una mirada (antes le recomiendo ver este excelente artículo para comprender más sobre el stack y las convenciones de llamadas). En el preincremento el código ensamblador (generado por GCC 3.4.5 para i386) es:
subl  $8, %esp          // reservamos 8 bytes en el stack (para variables locales)
movl $0, -4(%ebp) // a = 0
leal -4(%ebp), %eax // eax = &a
incl (%eax) // *eax= (*eax) + 1
movl -4(%ebp), %eax // eax = a
movl %eax, -8(%ebp) // b = eax
En el postincremento
subl  $8, %esp          // reservamos 8 bytes en el stack
movl $0, -4(%ebp) // a = 0
movl -4(%ebp), %edx // edx = a
leal -4(%ebp), %eax // eax = &a
incl (%eax) // *eax= (*eax) + 1
movl %edx, -8(%ebp) // b = edx
donde se ve que el registro edx se utiliza para guardar el valor que tenía a antes del incremento para luego asignárselo a b.

Pero la pregunta original es, ¿qué pasa si no usamos el valor de retorno? ¿el código de ++i o i++ es igual? Debería serlo, y de hecho, lo es. Pero como veremos más abajo, esto sólo se cumple si utilizamos tipos de datos built-in (int, double, long, etc.). Miremos el siguiente código:
void func() { }
void pre () { for (int c=0; c<8; ++c) { func(); } }
void post() { for (int c=0; c<8; c++) { func(); } }
int main()
{
pre();
post();
return 0;
}
Básico, un for pero con las dos variantes posible de incremento. El código generado para ambos casos (tanto para la función pre como post) es el siguiente:

pushl %ebp // guardar el viejo puntero a la base del stack
movl %esp, %ebp // establecer la nueva base del stack
subl $4, %esp // guardar 4 bytes en el stack (para variables locales)
movl $0, -4(%ebp) // c = 0
L3:
cmpl $7, -4(%ebp)
jg L2 // si c > 7 entonces ir a L2
call _func // llamar a la función func()
leal -4(%ebp), %eax // eax = &c
incl (%eax) // *eax= (*eax) + 1
jmp L3 // repetir yendo a L3
L2:
leave // restaurar la base del stack (popl %ebp)
ret // retornar al punto de llamada
Realmente es indiferente usar cualquiera de los dos tipos de incremento, salvo, en los tipos definidos por el usuario. C++ ofrece un soporte para tipos de usuario igual a los built-in (bueno, no del todo, pero lo están solucionando). Nos da la posibilidad de sobrecargar los operadores de nuestros propios tipos (clases). Por ejemplo:
class tipo {
int x;
public:
tipo(int y) : x(y) { }
tipo(const tipo& y) : x(y.x) { }
// preincremento
tipo& operator++() {
++x;
return *this;
}
// postincremento
tipo operator++(int) {
tipo tmp(*this);
++x;
return tmp;
}
bool operator<(int y) const { return x < y; }
};

void func() { }
void pre() { for (tipo c=0; c<8; ++c) { func(); } }
void post() { for (tipo c=0; c<8; c++) { func(); } }

int main()
{
post();
return 0;
}
No colocaré el código ensamblador por desbordar belleza, pero el código generado en este caso es distinto: cada incremento llama a la función correspondiente a su operador. Esto es debido a que ambas implementaciones varían considerablemente. Por ejemplo, el postincremento necesita de una instancia extra de tipo para poder devolver el anterior valor de this, en cambio, el preincremento devuelve una referencia al mismo this (sin necesidad de hacer una copia).

Como dato curioso, algo interesante ocurre al utilizar las optimizaciones del GCC. Si compilamos este último programa con el parámetro -O3, vamos a ver que todas las funciones correspondientes a la clase tipo desaparecen, y todo el código resultantes es inline, dando como resultado un código tan óptimo como si hubiéramos utilizado un int en vez de nuestra clase tipo.

¿Cómo obtengo el código ensamblador desde un archivo C/C++?
Con el compilador gcc, hay que utilizar el parámetro -S:
g++ -S -o archivo.s -c archivo.cpp
En archivo.s queda el código ensamblador (sintaxis AT&T).