Saltar al contenido

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

18 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 Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
Mostrando entradas con la etiqueta bytecode. Mostrar todas las entradas
Mostrando entradas con la etiqueta bytecode. Mostrar todas las entradas
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.