... Când evaluați performanțele aplicațiilor.

Am citit cu interes astăzi articolul lui Victor despre lucrul cu clasa Bitmap și diferențele surprinzătoare de performanță cauzate de două apeluri de funcție. Dacă nu ați citit încă articolul, chestiunea arată în rezumat cam așa:

Un obiect de tip System.Drawing.Bitmap este transformat (aici, în particular, o imagine color este transformată în alb-negru). Transformarea se face iterând peste fiecare pixel al imaginii și accesând direct datele care compun imaginea, folosind cod C# unsafe. Victor a observat că dacă scrie bucla în care se face prelucrarea așa:

int width = bmp.Width;
int height = bmp.Height;
for (int y = 0; y < height; y++)
    for (int x = 0; x < width; x++)
        [...]

... În loc de așa...

for (int y = 0; y < bmp.Height; y++)
    for (int x = 0; x < bmp.Width; x++)
        [...]

... Creșterea de performanță este semnificativă (de la 2 secunde la 0.3 secunde pe testul lui).

Dacă am înțeles bine, explicația oferită de Victor a fost că salvarea rezultatelor în două variabile locale face ca utilizarea lor să fie mult mai rapidă decât citirea lor repetată din memorie.

În vreme ce explicația este plauzibilă, pentru mine ea nu este satisfăcătoare, deoarece nu este susținută cu măsurători concrete. În plus, intuiția îmi spune că back-end-ul compilatorului JIT ar trebui să înlocuiască o instrucțiune de forma "x < bmp.get_Width()" (pentru JIT noțiunea de proprietate nu prea există) cu echivalentul lui "x < bmp.m_width", presupunând că obiectul Bitmap implementează proprietatea ca pe o fațadă pentru o variabilă internă; în funcție de metoda compilată, este posibil pentru JIT chiar și să salveze acea variabilă într-un registru, dacă este foarte des folosită. Chiar și dacă apelul de funcție nu ar fi fost substituit inline, diferența de viteză nu avea cum să fie atât de mare câtă vreme acea proprietate nu ar fi făcut altceva decât să returneze o valoare precalculată.

Ca să nu cad în același păcat, cel de a susține o ipoteză fără date, m-am pus pe măsurat. Am construit o aplicație de test cât mai asemănătoare, am compilat o versiune retail și am pus profilerul pe ea. Într-adevăr, pentru varianta care apela proprietatea la fiecare iterație a buclei, testul meu dura în medie 750 ms, pentru varianta care salva rezultatele în variabile locale, testul dura în medie 16 ms (cam de 47 de ori mai rapid), o îmbunătățire chiar mai spectaculoasă decât cea observată de Victor, cauzată probabil de faptul că testul meu a fost extrem de simplist, în vreme ce al lui bănuiesc că făcea operații mai complexe care mascau într-o oarecare măsură pierderea de viteză.

Folosind un profiler, însă, am aflat ceva ce nu aș fi putut descoperi altfel: Proprietățile Width și Height ale clasei Bitmap sunt doar wrappere pentru apeluri interne de funcții GDI+. Cu alte cuvinte, valorile returnate provin din cod nativ (unde sunt probabil stocate într-un câmp oarecare) și trec prin nivelul P/Invoke din CLR. Mai mult, dacă e să dau crezare profilerului, implementarea nativă a funcțiilor este thread-safe, și o bună parte din timp este petrecut de fapt în mecanismul de sincronizare.

Ca să nu mă lungesc prea mult cu vorba, am de fapt de spus două lucruri.

În primul rând, aveți grijă cum utilizați proprietățile în .NET. Sunt deosebit de înșelătoare, deoarece sunt construite în așa fel încât să pară variabile, dar sunt de fapt apeluri de funcție. Asta înseamnă că nu e obligatoriu să fie pur și simplu fațade pentru niște variabile interne, ci pot fi apeluri extrem de complexe, care conțin sincronizare, efecte colaterale și toate bucuriile oferite de programarea imperativă în general. Exemplul meu favorit sunt expresiile de forma "a.X += b.Y", în care unii programatori nici nu realizează că se ascund trei apeluri de funcție (plus o adunare și o atribuire ca bonus).

În al doilea rând, atunci când întâmpinați probleme de performanță, sau pur și simplu diferențe inexplicabile, nu aruncați cu ipoteze fără să le susțineți cu date. Ca regulă generală în domeniul performanțelor aplicațiilor, dacă ați găsit o explicație pentru un fenomen oarecare pe căi pur teoretice, e foarte probabil ca explicația să fie greșită.