Vamos a proseguir con las comparativas que iniciamos en WinAPI vs C en donde si recordáis comparábamos las funciones de la RTL de los compiladores de Visual C++ 2017 y C++ Builder 10.2.3 contra las funciones de la API de Windows.
Hoy vamos a centrarnos en Visual C++, y lo que procederemos será a comparar el rendimiento de las diferentes formas que permiten rellenar un bloque de memoria con un valor determinado. Vamos a usar FillMemory (API de Windows); memset (RTL de C/C++); __stosd (intrínseca de Visual C++); implementación en C puro, e implementación en ensamblador puro.
Los resultados inicialmente han sido previsibles, la implementación en C++, pese a usar 32 bits se ha mostrado notablemente más lenta. En un segundo nivel la API de Windows demuestra su rendimiento algo peor como ya vimos. Las otras implementaciones son muy similares.
Tiempo (ms) | |
FillMemory() | 5.187 |
FillMemory() | 4.609 |
__stosd | 4.563 |
C++ | 38.031 |
Ensamblador | 4.610 |
Sin embargo si probamos lo mismo en modo Release, y con todas las optimizaciones activadas, las cifras cambian notablemente:
Tiempo (ms) | |
FillMemory() | 4.500 |
FillMemory() | 4.344 |
__stosd | 4.328 |
C++ | 4.328 |
Ensamblador | 4.313 |
Todas las implementaciones tienen una velocidad de ejecución similar, incluyendo la versión en C, de manera que en proceso tan simples, el compilador es suficientemente listo como para generar el mejor código posible.
En las pruebas no he entrado en más combinaciones, por ejemplo usando movsb que pese a funcionar byte a byte, debido a las optimizaciones de Intel está más optimizado. Sí que os puedo decir que el beneficio marginal estaba en menos de 5%.
Podéis revisar el código fuente completo a continuación, o descargarlo junto al ejecutable aquí (64 Kb. en formato ZIP).
// -----------------------------------------------------------------------------
#pragma hdrstop
// -----------------------------------------------------------------------------
#include
#include
#include
#include
#include
#include
// -----------------------------------------------------------------------------
#define KI_ITER 1000
#define KI_SIZE 64*1024*1024L
// -----------------------------------------------------------------------------
int _tmain(int argc, _TCHAR* argv[])
{
unsigned long lStart, lStop;
char *acBuffer = new char[KI_SIZE + 16];
//FillMemory
lStart = GetTickCount();
for (unsigned int iCount = 0; iCount < KI_ITER; iCount++)
{
FillMemory(acBuffer, KI_SIZE, iCount);
}
lStop = GetTickCount();
_tprintf(_T("FillMemory(): %ld ms\n"), lStop - lStart);
//memset
lStart = GetTickCount();
for (unsigned int iCount = 0; iCount < KI_ITER; iCount++)
{
memset(acBuffer, iCount, KI_SIZE);
}
lStop = GetTickCount();
_tprintf(_T("memset(): %ld ms\n"), lStop - lStart);
//__stosd
lStart = GetTickCount();
for (unsigned int iCount = 0; iCount < KI_ITER; iCount++)
{
__stosd((unsigned long *) acBuffer, iCount, KI_SIZE / 4);
}
lStop = GetTickCount();
_tprintf(_T("__stosd(): %ld ms\n"), lStop - lStart);
//C
lStart = GetTickCount();
for (unsigned int iCount = 0; iCount < KI_ITER; iCount++)
{
for (unsigned int iC = 0; iC < KI_SIZE / 4; iC++)
{
((unsigned long *) acBuffer)[iC] = iCount;
}
}
lStop = GetTickCount();
_tprintf(_T("C: %ld ms\n"), lStop - lStart);
//asm
lStart = GetTickCount();
_asm
{
mov edx, KI_ITER
stosd_repeat:
mov eax, edx
mov ecx, KI_SIZE / 4
mov edi, acBuffer
rep stosd
dec edx
jnz stosd_repeat
}
lStop = GetTickCount();
_tprintf(_T("_asm: %ld ms\n"), lStop - lStart);
delete[] acBuffer;
getchar();
return(0);
}
// -----------------------------------------------------------------------------
No deja de tener mucho sentido que la función hecha a mano sea inmensamente más lenta en Debug para luego en Release igualarse en tiempos con sus contrapartidas. Al fin y al cabo las funciones «de librería» ya están extensamente probadas, por lo que Debug no mete gran cosa en las iteraciones, sin embargo en el código propio si que Debug se entromete, ya que ese es el cometido, poder depurar los posibles fallos.
Dicho lo anterior con mi gran desconocimiento de la arquitectura de los compiladores…
Exacto Fernando esa es la diferencia. El código de usuario en debug se interrumpe continuamente para poder ser depurado, mientras que las funciones de la librería no lo hacen.
No conozco bien C++, pero quizás al compilar con debug se esté haciendo un seguimiento de los bloques de memoria solicitados para comprobar si son liberados correctamente, y ese «tracking» tiene un coste.
Con Harbour pasa exactamente eso. Cuando compilas con debug, al cerrar el programa te informa de los bloques de memoria sin liberar, mientras que si compilas sin debug no.
Así mismo Jose. En debug comprueba los memory leaks, y también los buffer overrun y underrun. No he usado Harbour, pero si xHarbour, que recuerdo que generaba código C que luego era compilado, por lo que esencialmente debe ser lo mismo.