Az előző cikkben az egyik mintakódban láthattuk, hogy a jitter milyen szépen inline-olta a Console.WriteLine() metódust. Kíváncsi lettem hát, hogy mi a stratégiája. Azt korábbról tudtam, hogy az egyszerű property getter/settereket binline-olja, de vajon mitől függ, hogy mivel teszi meg, és mivel nem?
Itt van például a következő kód:
static int Calc(int a, int b)
{
int result = 0;
if (a < b)
{
result += a * b;
}
else
{
result += a + b;
}
return result;
}
static void Main(string[] args)
{
int result = Calc(3, 8);
Console.WriteLine(result);
}
Vajon a Calc() metódushívás be lesz inline-olva? Nézzük mit alkotott a jitter:
// Verem állapot elmentése 00000000 push ebp 00000001 mov ebp,esp // Valamiért az esi fontos a hívónak, // nekünk most ez nem számít 00000003 push esi // Ez csak a debugger behúzása 00000004 call 65A67D40 // Hopp, és itt az inline-olt Calc(). // De nem csak inline-olt, hanem szélsőségesen optimalizált! // Nézzük: // Egy változó nullázása, alighanem // result = 0 00000009 xor esi,esi // Ha esi regiszter a result változó, akkor itt ahhoz ad hozzá 0x18-at, azaz 24-et, // result += 24 0000000b add esi,18h // Ez itt már a Console.WriteLine, ugyancsak inline-olva 0000000e call 65457130 00000013 mov ecx,eax 00000015 mov edx,esi 00000017 mov eax,dword ptr [ecx] 00000019 mov eax,dword ptr [eax+38h] 0000001c call dword ptr [eax+14h] // Vissza a hívóhoz 0000001f pop esi 00000020 pop ebp 00000021 ret
Mit látunk? Nem hogy inline lett a Calc() metódus, hanem jelentős optimalizáláson ment keresztül. A jitter észrevette, hogy a konstans paraméterek miatt a Calc() metódusban található feltételes elágazás mindig egyirányú, így ezt kitörölte. Azt is észrevette, hogy a konstans paraméterekkel elvégzendő művelet előre kiszámolható, így azt meg is tette. Ez figyelemre méltó teljesítmény! Ezek után kicsit furcsa, hogy miért nullázgat változót a 17-es sorban, majd végzi el az összeadást a kód a 21-ben, amikor egyből beemelhetné a 24-et az esi regiszterbe.
Hogy elvegyük az extrém optimalizálás lehetőségét a jittertől, vigyünk bele egy kis véletlent az átadott paraméterekbe:
static void Main(string[] args)
{
int i = new Random().Next();
int result = Calc(3 + i, 8 + i);
}
A generált kód egy kicsit hosszabb volt a Random kezelése miatt, azokat a kódokat kitöröltem:
// Verem állapot elmentése 00000000 push ebp 00000001 mov ebp,esp // Valamiért az esi még mindig fontos a hívónak, // nekünk most ez nem számít 00000003 push esi // Debugger: 00000004 call 65C97D40 // Itt történt a random példányosítása, konstruktor // hívása, a Next() virtuális metórus slot-jának // keresése. // Az alábbi a Next() hívása: 0000002a call dword ptr [eax+14h] // A Next() az eredményt az EAX-ba adta vissza, tehát // az EAX most egy random szám, ez került a C# kódban az // i változóba. // Az első paramétere a Calc()-nak az i + 3. Erre a // kézenfekvő megoldás az lett volna, ha a jitter // előbb ecx-be tölti az eax-ot, majd egy add utasítással // elvégzi az összeadást. Ehelyett az eredetileg nem arra való // LEA (load effective address) utasítást használja, mivel az // a mindenféle támogatott címzési trükk miatt tud aritmetikai // műveleteket végezni, és azonnal az eredményt egy regiszterben // tárolni. Így meg lehetett spórolni egy utasítást: 0000002d lea ecx,[eax+3] // A Calc() második paramétere i + 8. Azt nem tudom, hogy itt // miért nincs használva a LEA-s trükk. 00000030 add eax,8 00000033 mov edx,eax // Hopp, nincs inline!!! Calc() hívása: 00000035 call dword ptr ds:[00143818h] // Eredmény az eax-ben jött vissza, ez már csak az // inline-olt Console.WriteLine(), meg a visszatérés 0000003b mov esi,eax 0000003d call 65687130 00000042 mov ecx,eax 00000044 mov edx,esi 00000046 mov eax,dword ptr [ecx] 00000048 mov eax,dword ptr [eax+38h] 0000004b call dword ptr [eax+14h] 0000004e pop esi 0000004f pop ebp 00000050 ret
Az a meglepő eredmény, hogy valamiért most nem került inline-olásra ugyan az a metódus. Szerencsére sikerült megtalálnom ezt a remek blog bejegyzést, ami megmagyarázza miről van szó.
A jitter egy mérőszámot próbál kalkulálni arról, hogy megéri-e az inline-olás vagy nem. Az első esetben az inline-olandó kódot olyan picire tudta optimalizálni, hogy ott már az inline-olás megérte. A második esetben nem sikerült kidobálni az if-eket, és a jóval nagyobb eredeti kód már nem éri el a küszöböt. A blog azt is megemlíti, hogy a jitter nagyobb hajlandóságot mutat az inline-ra, ha a hívás egy ciklusban van, ekkor ugyanis a metódushívás költsége a többszörös hívás miatt jobban érvényesülhet. Próbáljuk hát ki:
int result = 0;
for (int i = 0; i < 10; i++)
{
result += Calc(3 + i, 8 + i);
}
// Verem állapot és fontos regiszterek elmentése 00000000 push ebp 00000001 mov ebp,esp 00000003 push edi 00000004 push esi 00000005 push ebx // Debugger behúzása 00000006 call 65A97D40 // Main() lokális változója: result = 0 0000000b xor ebx,ebx // for ciklus, inicializálás: // i = 0 0000000d xor esi,esi // Calc() paraméterei: i + 3, i + 8 // mivel tényleg megtörtént az inline, // ezek már a Calc() argumentumait hordozzák. // a -> eax, b -> edx 0000000f lea eax,[esi+3] 00000012 lea edx,[esi+8] // Calc() lokális változója: result = 0 00000015 xor edi,edi // if (a < b), ha nem ugrás 22-re 00000017 cmp eax,edx 00000019 jge 00000022 // result += a * b 0000001b imul edx,eax 0000001e add edi,edx 00000020 jmp 00000026 // else ág, result += a + b 00000022 add eax,edx 00000024 mov edi,eax // Main.result += Calc.result 00000026 add ebx,edi // for ciklus: // i++ 00000028 inc esi // i < 10? 00000029 cmp esi,0Ah // ha nincs vége a ciklusnak, újra 0000002c jl 0000000F // Ez már csak a Console.WriteLine() 0000002e call 65487130 00000033 mov ecx,eax 00000035 mov edx,ebx 00000037 mov eax,dword ptr [ecx] 00000039 mov eax,dword ptr [eax+38h] 0000003c call dword ptr [eax+14h] 0000003f pop ebx 00000040 pop esi 00000041 pop edi 00000042 pop ebp 00000043 ret
A ciklus tehát valóban arra sarkalta a jittert, hogy a lineáris lefutással szemben itt már végezze el az inline-t. Az is látszik, hogy a nagyobb kódon a jitter azért nem boldogul olyan jól az optimalizálással. A Calc() metódus Result változójára, amit a gépikódban az EDI regiszter hordoz, nem lenne szükség, és ezt emberként nem olyan nehéz észrevenni.
Konklúzió
A C# nyelv nem rendelkezik a C++ inline kulcsszavával. Ez néhány esetben hátrány lehet, legtöbbször azonban nem az. A mai processzoroknál nagyon nehéz kiszámítani, hogy mitől lesz gyorsabb egy kód, és nem biztos, hogy egy függvényhívás költsége olyan jelentős, mint ahogy azt gondolnánk.
Láthatólag a jitter-be próbáltak egyfajta heurisztikát építeni, amely eldönti, mikor lehet érdemes inline-olni. Biztos találni sok helyzetet, amikor a jitter nem a legjobb döntést hozza. Nincs is ideje túl sokat analizálni, különben az analizáláson több idő megy el, mint amit az optimális kóddal nyerni tud. Átlagos helyzetekben azonban valószínűleg megállja a helyét.
