Autor en Google+
Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET, ASP.NET Core, MVC, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

10 años online 🎂

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET, ASP.NET Core, MVC, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 3 de julio de 2007

Sí, he de admitir que es una pregunta rara, pero me la hago desde que conocí hace ya muchos años que en lenguaje C podía incrementar una variable de tres formas, autoincrementando (var++), incrementando en uno (var+=1) o sumando la unidad (var=var+1)... ¿hay alguna diferencia entre ellas? Esta absurda inquietud la he mantenido durante lustros, viendo cómo estas construcciones se propagaban hacia lenguajes más actuales con sintaxis basada en C, como C++, C# o Java.

Claro está que desde el punto de vista semántico no existe diferencia alguna, pues todas comparten el mismo objetivo: incrementar en una unidad la variable indicada. Sin embargo, ¿existe diferencia en la forma en que el compilador las trata? ¿se genera código más eficiente utilizando una de ellas, o son absolutamente iguales?

Para responder a esta cuestión, he realizado una pequeña aplicación en varios lenguajes y plataformas, generado posteriormente un ejecutable y desensamblado el binario de salida con la herramienta correspondiente en cada caso. A continuación describo los resultados obtenidos.

En primer lugar, he atacado al lenguaje C, compilado bajo Linux con gcc y desensamblando con ndisasm.

La aplicación con la que he realizado las comprobaciones era la siguiente:

#include "stdio.h"
main()
{
int i;
i = 0x99f;
i++;
i += 1;
i = i + 1;
printf("Vale: %d", i);
}

Nota: el 0x99f con el que inicializo la variable "i" me sirve para buscar rápidamente en el código ensamblador dónde se encuentran las instrucciones a estudiar.

Bueno, a lo que vamos. Una vez compilado y desensamblado, el código que encuentro es:

...
mov word [di-0x8], 0x99f
add [bx+si], al
add word [di-0x8], byte +0x1
add word [di-0x8], byte +0x1
add word [di-0x8], byte +0x1
...

Según parece, el compilador gcc ha generado exactamente el mismo código en todos los casos. La verdad es que este resultado me ha sorprendido un poco, esperaba que se utilizaran en los primeros casos las instrucciones de incremento directo del procesador, optimizando el binario generado, pero no ha sido así. A la vista de esto, he preguntado a Google y parece ser que es un comportamiento recomendado para los compiladores de C, por lo que podemos dar por concluida esta línea de la investigación.

A continuación he pasado a C#, compilando tanto con .NET Framework de Microsoft como con Mono. La aplicación era similar a la anterior (traducida, obviamente), y el resultado en lenguaje intermedio (MSIL), obtenido tanto con ildasm como con su equivalente monodis es el mismo:

[...]
//000011: int i = 0x99f;
IL_0001: ldc.i4 0x99f
IL_0006: stloc.0
//000012: i++;
IL_0007: ldloc.0
IL_0008: ldc.i4.1
IL_0009: add
IL_000a: stloc.0
//000013: i += 1;
IL_000b: ldloc.0
IL_000c: ldc.i4.1
IL_000d: add
IL_000e: stloc.0
//000014: i = i + 1;
IL_000f: ldloc.0
IL_0010: ldc.i4.1
IL_0011: add
IL_0012: stloc.0
[...]

Podemos observar que, como en el caso anterior, el compilador no diferencia entre el tipo de incremento que utilicemos, da exactamente igual.

Por último, probamos con Java 5, utilizando el JDK de Sun. La aplicación es exactamente igual a las anteriores, con la correspondiente adaptación a este lenguaje, algo así como:

class Prueba
{
public static void main(String[] args)
{
int i = 0x99f;
i++;
i+=1;
i=i+1;
System.out.println("Vale: " + i);
}
}

El compilador de Java, en este caso, sí hace distinciones entre los dos primeros modelos de incremento y el tercero. El autoincremento e incremento los genera exactamente iguales, creando una instrucción bytecode de incremento en una unidad, mientras que el último lo crea como una suma; supongo que la primera será posible optimizarla y su ejecución será más rápida:

 0: sipush 2463
3: istore_1
// i++:
4: iinc 1, 1
// i+=1:
7: iinc 1, 1
// i=i+1:
10: iload_1
11: iconst_1
12: iadd
13: istore_1
// Resto...
[...]

En resumidas cuentas, podemos usar con toda tranquilidad cualquiera de los operadores de suma para incrementar una variable, puesto que el código será igual de eficiente (o poco eficiente, según se mire) y prácticamente no tendrá incidencia en la velocidad de ejecución, al menos con las opciones de compilación por defecto. Sólo Java parece estar adelantado en este sentido, pues es capaz de distinguir estos casos.

Y sí, era una inquietud de lo más absurda, lo dije desde el principio, pero me he quedado muy tranquilo, y con una duda menos en mi saco de ignorancia.

Estos contenidos se publican bajo una licencia de Creative Commons Licencia Reconocimiento-No comercial-Compartir bajo la misma licencia 3.0 España de Creative Commons

2 Comentarios:

Anónimo dijo...

Existen algunas diferencias.

A ver:


var++
var=var+1


En teoria son iguales, a priori un compilador "decente" debería optimizar el segundo caso generando el mismo código que el primero, como ocurre con los compiladores que comentas, pero existen diferencias notables entre ambas fórmulas ;)

Y me explico:

Cuando tu usas var++ lo que estás diciendo es: "Incrementa la variable en uno", sin embargo cuando dices var=var+1 lo que estás literalmente diciendo es: "Calcula el valor de esta variable, añádele uno y luego almacena el resultado de nuevo en la variable". Por tanto, en contra de lo que comentas en el post, sí existe una diferencia semántica entre ambos casos.

Muchas arquitecturas de CPUs, y en particular las "x86" disponen de una manera específica para incrementar una variable in-situ (esto es, para incrementar "algo que existe") utilizando una instrucción simple, por lo tanto la primera forma puede ser implementada diréctamente con una simple operación del procesador, la segunda forma es una asignación genérica, en la cual el compilador pone el foco en el cálculo del valor de la expresión de la derecha (esto significa que tendrá que copiar el valor de var en una variable temporal antes de añadir algo a ella, porque no puede "desprenderse" del valor original de var), después, una vez que tiene el resultado lo moverá a la parte izquierda de la asignación (moviendo el valor a var). Esto, por supuesto es una redundancia.

Un compilador decente, como los que mencionas, se dará cuenta de que existe esta redundancia y generará el código correspondiente a la primera fórmula (alterando directamente la variable in-situ). Evidentemente, si hablamos de optimización, teóricamente no deberíamos depender de la "inteligencia del compilador" a la hora de escribir nuestro código.

En general, para cualquier operación cuando eliges entre una forma genérica y una particular que ha sido diseñada para una resolver una necesidad específica, normalmente la fórmula "particular" estará mucho más optimizada que la genérica para esa resolver esa necesidad, y estará desarrollada precísamente para ella. Por supuesto que el caso general puede ser optimizado una vez que el compilador "se de cuenta" de este caso particular que comentas pero... ¿Porqué confiar en la optimización cuando puedes especificar con precisión qué es lo que realmente quieres hacer?

Profundizando más, y ya resuelvo tu curiosidad del todo:

Cuando el procesador va a incrementar una variable en uno, en tu ejemplo con var=var+1, para esa operación existe una instrucción mucho más específica: ésta es var++ y me explico, el operador ++ significa específicamente el incremento de de la variable en uno, en oposición a el genérico +=uno el cual incrementa la variable en un valor determinado, Esto marca la diferencia entre ambos casos en arquitecturas como la x86 donde existe una instrucción específica para incrementar un registro de memoria en uno (la instruccion inc en ensamblador), la cual es mucho más específica y optimizada, como sabrás, que la instrucción para incrementar un valor genérico a una variable (la instrucción add en ensamblador), por supuesto, y de nuevo,un compilador en condiciones debería darse cuenta de este caso particular y utilizar la instrucción más eficiente.

Nada más, espero haber matado para siempre el "gusanillo" de tu curiosidad.

Salu2 y enhorabuena por tu interesante bLog.

Miguel Del Valle.-

pablasso dijo...

Miguel, creo que te aventaste un rollo de mas, la duda ya estaba resuelta con la prueba.

La duda era mas bien aclarar si los compiladores hacen igual de efectivo un codigo que se puede escribir desde diferentes caminos y con mismos resultados.

i++ en lugar de i=i+1 puede ser mas optimo, pero a final de cuentas da igual, lo que nos importa en verdad es la agilidad del codigo compilado, no del codigo por compilar.