Tal vez recordéis Sieve en PowerBASIC Console Compiler 6 donde comparaba la última versión de PowerBasic Console Compiler (6.04) de 2011, con la anterior (5.0) de 2008.
Continuando con el artículo de WinAPI contra C, intrínsecas, C++ y ensamblador donde evaluábamos diferentes mecanismos de hacer lo mismo sobre Windows, hoy compararemos la evolución que ha tenido Microsoft Visual C++ en estos 11 años.
Pues aprovechando que me he encontrado de casualidad con los ejecutables de Visual C++ 2008 que publiqué en x86 vs x64 he procedido a comparar el rendimiento con el último Visual C++ 2017. Me hubiera gustado hacerlo sobre Sieve en C, pero creo que este ejemplo mucho más sencillo y en C portable será más claro.
El código fuente original, era bien sencillo, un bucle que se ejecuta 10.000.000.000 veces y realiza una suma y un decremento:
#include
#include
void __cdecl main (void)
{
unsigned __int64 iCount, iRes;
unsigned int iInicio, iFin;
iInicio = clock();
iRes = 0;
for (iCount = 1; iCount <= 10000000000; iCount++)
{
iRes += iCount;
iRes--;
}
iFin = clock();
printf("%I64d %d\n", iRes, iFin - iInicio);
getchar();
}
Los resultados han sido magníficos:
Compilador | Tamaño ejecutable (bytes) | Tiempo de ejecución (ms) |
Visual C++ 2008 (9.0) | 66.048 | 7.531 |
Visual C++ 2017 (14.14) | 116.736 | 4.984 |
La velocidad del ejecución del código generado con Visual C++ 14.1 (2017) casi dobla a la del que generaba 11 años antes. Desgraciadamente también el tamaño del ejecutable ha crecido en la misma proporción. Obviamente el auge x64 es en donde mayores mejoras vamos a notar, pero no deja de ser curioso que el mismo código que escribiéramos hace 10 años, pueda correr el doble de rápido con sólo recompilarlo.
Analicemos un poco más en detalle lo que ha ocurrido. Visual C++ 2008 lo convertía al siguiente código ensamblador:
xor ebx, ebx
mov ecx, 1
mov edi, eax
mov rax, 10000000000
$LL3@main:
lea rbx, QWORD PTR [rbx+rcx-1]
inc rcx
cmp rcx, rax
jbe SHORT $LL3@main
Y en Visual C++ 2017 es así:
xor ebx, ebx
mov rax, 10000000000
mov ecx, 1
$LL4@main:
dec rbx
add rbx, rcx
inc rcx
cmp rcx, rax
jbe SHORT $LL4@main
Viéndolo así, se entiende fácilmente. Porque Visual C++ 2017 es mucho más listo y evita el uso de LEA para realizar el cálculo. Aunque es eficiente, es poco paralelizable en la pipeline, en donde triunfan las instrucciones sencillas tipo RISC.
Como siempre hago, puedes descargarte el código fuente y los ejecutables aquí (90 Kb. en formato ZIP).
¿Os cobran espacio a los programadores de C si en lugar de en una líneas, hacéis lo mismo en tres? Es un gran misterio que no se si algún día se resolverá. O quiza es que los teclados para C++ no llevan barra espaciadora…
Mucho mejor C++ 2008, al hacer ejecutables más compactos. Supongo que si alguien quisiera un ensamblador más efectivo, lo haría directamente en ensamblador, no? (claro, hoy en día no hay valor, que lo haga C++ XD)
Interesante artículo, gracias.
Pregunta: como haces para saber que VC convierte ese codigo en ensamblador? (como sabes que ese es el codigo que genera)
Muchas gracias Manuel.
Casi todos loc ompiladores de C/C++ tienen esa opción. En Visual C++ se llama «Generate Assembly Listing» en el IDE. /FA por linea de comandos.
/FA, /Fa (Listing File).
ok, muchas gracias por la explicación, muy buen artículo.
bianamaran creo que el problema del tamaño es porque son sólo unas pocas lineas de código, supongo que si fueran un centenar de líneas podríamos comprobar que la diferencia de tamaño no es tanta.
bianamaran, no entiendo lo de los espacios. Te refieres a espacios o saltos de linea? De espacios, suelo poner bastantes, salvo después de los paréntesis que no me gusta mucho. De saltos de linea o returns, también creo que hay bastantes, la excepción es este post, donde para facilitar que quepa en la pantalla he ahorrado algunos retornos de carro.
¿Quizás te molesta la forma de indentar las llaves?
La verdad es que si el código está bien estructurado e identado no suelo fijarme en los detalles del estilo.
Personalmente prefiero identar las llaves de esta manera
while (true) {
$i++;
}
Pero como digo, he leído tanto código de extraños que me adapto automáticamente a cualquier estilo que se pueda considerar «correcto»: limpio y ordenado.
Creo que bianamaran se refiere a la forma de declarar las variables, personalmente no me convence declarar varias variables en una línea, pero también lo he visto mucho y es una forma de saber de forma rápida que variables son de un tipo y cuales de otro.
Los que indentamos como yo en C Fernando somos minoría. La mayoría lo hacen como tu. En cualquier caso estamos empatados, a fuerza de ir viendo código y manteniéndolo, estoy habituado a manejarlo independientemente del formato. Lo importante es como dices que esté bien escrito y bien pensado.
Declarar variables de ese modo me ayuda a agruparlas por tipo, es algo que suelo hacer, aunque efectivamente todo se compensa. Sin embargo declarándolas una por linea, si lo haces a principio del método o función, conlleva que se llena la pantalla de variables y a la hora de leer el código pierdes el contexto que la usa.
Al final no existe el sistema perfecto…