Debug és release módú fordítás

Ha egy program futásának sebességét akarjuk megmérni, mindig fontos, hogy release módban fordítsuk le a programot és ne debug módban. A következőkben megmutatom, miért.

Visual Studio alatt egy program lefordított verzióját könnyen meg lehet nézni: le kell rakni egy töréspontot, majd amikor ott megáll a debuggolás közben, akkor a Debug/Windows/Disassembly (Alt + 8) menüpontot választva megnézhetjük minden egyes C++ forráskód sorhoz, hogy mire fordult le.

Az első próbálkozás

Először az alábbi programmal próbálkoztam, mondván, hogy ez már tartalmaz annyi műveletet, hogy lássunk is valamit.

#include <iostream>

using namespace std;

int main()
{
	int sum = 0;
	for (int i = 0; i<10; i++)
	{
		sum += i;
	}
	cout << "sum: " << sum << endl;
	return 0;
}

No ez nem jött be. Release módban a fordító az egész for ciklus eredményét (45, vagyis 0x2D) kiszámolta előre és az eredményt beleírta a lefordított kódba:

	int sum = 0;
	for (int i = 0; i<10; i++)
	{
		sum += i;
	}
	cout << "sum: " << sum << endl;
001412A0  mov         ecx,dword ptr ds:[143054h]  
001412A6  push        141980h  
001412AB  push        2Dh  
001412AD  call        std::operator<<<std::char_traits<char> > (0141760h)  
001412B2  mov         ecx,eax  
001412B4  call        dword ptr ds:[143034h]  
001412BA  mov         ecx,eax  
001412BC  call        dword ptr ds:[14305Ch]  
	return 0;
001412C2  xor         eax,eax  
}
001412C4  ret  

Ennél szemmel láthatóan bonyolultabb dolgot kell számoltatni. (De emellett azért vegyük észre, hogy ez nem kis eredmény a fordítótól, még ha manapság ezt már el is várjuk.)

A magic

No akkor tegyünk bele egy magic függvényt, amit nem tud ilyen könnyen kiszámolni. Szinusz táblája már csak nincsen:

#include <iostream>

using namespace std;

int magic(int x)
{
	return 10.0F * sin(x);
}

int main()
{
	int sum = 0;
	for (int i = 0; i<10; i++)
	{
		sum += magic(i);
	}
	cout << "sum: " << sum << endl;
	return 0;
}

Kezdjük a debug fordítással. Ennek eredménye a következő:

int main()
{
00DE5640  push        ebp  
00DE5641  mov         ebp,esp  
00DE5643  sub         esp,0D8h  
00DE5649  push        ebx  
00DE564A  push        esi  
00DE564B  push        edi  
00DE564C  lea         edi,[ebp-0D8h]  
00DE5652  mov         ecx,36h  
00DE5657  mov         eax,0CCCCCCCCh  
00DE565C  rep stos    dword ptr es:[edi]  
	int sum = 0;
00DE565E  mov         dword ptr [sum],0  
	for (int i = 0; i<10; i++)
00DE5665  mov         dword ptr [ebp-14h],0  
00DE566C  jmp         main+37h (0DE5677h)  
00DE566E  mov         eax,dword ptr [ebp-14h]  
00DE5671  add         eax,1  
00DE5674  mov         dword ptr [ebp-14h],eax  
00DE5677  cmp         dword ptr [ebp-14h],0Ah  
00DE567B  jge         main+51h (0DE5691h)  
	{
		sum += magic(i);
00DE567D  mov         eax,dword ptr [ebp-14h]  
00DE5680  push        eax  
00DE5681  call        magic (0DE14A6h)  
00DE5686  add         esp,4  
00DE5689  add         eax,dword ptr [sum]  
00DE568C  mov         dword ptr [sum],eax  
	}
00DE568F  jmp         main+2Eh (0DE566Eh)  
	cout << "sum: " << sum << endl;
...

A cout utáni részeket már levágtam, mert nem az a lényeg. Ezután pedig a release fordítás eredménye:

int main()
{
003712A0  push        esi  
003712A1  push        edi  
	int sum = 0;
003712A2  xor         edi,edi  
	for (int i = 0; i<10; i++)
003712A4  xor         esi,esi  
003712A6  jmp         main+10h (03712B0h)  
003712A8  lea         esp,[esp]  
003712AF  nop  
003712B0  movd        xmm0,esi  
	{
		sum += magic(i);
003712B4  cvtdq2pd    xmm0,xmm0  
003712B8  call        __libm_sse2_sin_precise (0372346h)  
003712BD  mulsd       xmm0,mmword ptr ds:[373260h]  
	for (int i = 0; i<10; i++)
003712C5  inc         esi  
	{
		sum += magic(i);
003712C6  cvttsd2si   eax,xmm0  
003712CA  add         edi,eax  
003712CC  cmp         esi,0Ah  
003712CF  jl          main+10h (03712B0h)  
	}
	cout << "sum: " << sum << endl;
...

Láthatjuk, hogy a debug fordítás sokkal több pakolgatást, másolgatást tartalmaz. Például a ciklusmagban minden iterációban a memóriából betölti i értékét (ő a “dword ptr [ebp-14h]”), lerakja a stackre, mint a magic függvény paramétere, utána meghívja a függvényt és még a sum értékét is kiírja a memóriába. Ennek az az oka, hogy debuggolás közben minden részeredmény a memóriában legyen és minden soron meg tudjuk állni. Release módban egy csomó forráskód sorhoz nincs is natív kód, így ha ott próbálunk meg soronként ugrálni (vagyis a debuggert megkérni, hogy a következő sornak megfelelő gépi kódú parancsig fusson), akkor kisse össze-vissza fog ugrálni.

Hibaellenőrző kódrészek

Befejezésül még kíváncsiságból megnéztem, milyen a magic függvény. Ez a memóriában egy kicsit előrébb helyezkedik el, cseles módon a “call magic (0DE14A6h)” által hivatkozott címen csak egy ugrás van a függvény tényleges helyére.

int magic(int x)
{
00DE42F0  push        ebp  
00DE42F1  mov         ebp,esp  
00DE42F3  sub         esp,0C8h  
00DE42F9  push        ebx  
00DE42FA  push        esi  
00DE42FB  push        edi  
00DE42FC  lea         edi,[ebp-0C8h]  
00DE4302  mov         ecx,32h  
00DE4307  mov         eax,0CCCCCCCCh  
00DE430C  rep stos    dword ptr es:[edi]  
	return 10.0F * sin(x);
00DE430E  mov         eax,dword ptr [x]  
00DE4311  push        eax  
00DE4312  call        sin<int> (0DE14A1h)  
00DE4317  add         esp,4  
00DE431A  fstp        qword ptr [ebp-0C8h]  
00DE4320  movsd       xmm0,mmword ptr [ebp-0C8h]  
00DE4328  mulsd       xmm0,mmword ptr ds:[0DEC9B8h]  
00DE4330  cvttsd2si   eax,xmm0  
}
00DE4334  pop         edi  
00DE4335  pop         esi  
00DE4336  pop         ebx  
00DE4337  add         esp,0C8h  
00DE433D  cmp         ebp,esp  
00DE433F  call        __RTC_CheckEsp (0DE132Fh)  
00DE4344  mov         esp,ebp  
00DE4346  pop         ebp  
00DE4347  ret

A release mód nem is használ függvényhívást, csak a szinusz kiszámításához. Ehhez képest itt meg egy csomó push-pop páros van, a szokásos memória másolások, valamint egy érdekes ellenőrzés:

00DE42F0  push        ebp  
00DE42F1  mov         ebp,esp  
...
00DE433D  cmp         ebp,esp  
00DE433F  call        __RTC_CheckEsp (0DE132Fh)  
00DE4344  mov         esp,ebp  
00DE4346  pop         ebp  

A függvény elején elmenti a bázispointerbe (EBP) a stack pointert (ESP) (amihez a legelején az EBP értékét a stackre elmenti, a levégén meg visszaolvassa), a függvény befejezése előtt pedig van egy ellenőrzés, hogy a stack pointer tényleg oda állt-e vissza, ahol a függvény elején volt. Ha nem, akkor meghívja az RTC_CheckEsp függvényt, ami valószínűleg a hiba jelzésére szolgál.

A konklúzió tehát az, hogy debug módban azért nem mérünk futási időt, mert rengeteg olyan műveletet tartalmaz, ami az éles futáshoz egyáltalán nem kell. Ráadásul debug módban a fordító összes optimalizáló funkciója is ki van kapcsolva.

Végezetül megjegyzem, hogy egyszer a Point Cloud Library használata során tapasztaltuk, hogy a debug és release módú fordítás és futtatás között szó szerint 1000-szeres sebességkülönbség volt! Mivel a program 17 millió (lézerszkennerrel felvett) pontot elemezgetett, nyilván nagyon nem volt mindegy, hogy a ciklusokban mennyi többletfeladat van.

Szerzők, verziók: Csorba Kristóf