Performance & Best Practices

In acest articol voi discuta despre performante si practici recomandate in .NET si WPF.

.NET / C#
1. Folositi clasa Stopwatch pentru a masura performantele
2. Concatenarea obiectelor String e costisitoare
3. String.Equals de preferat operatorului ==
4. 'as' si 'is' versus type casting
5. Minimizati numarul de parametrii ai unei metode
6. Minimizati numarul de variabile locale
7. readonly versus const
8. Folositi ConditionalAttribute in locul directivei #if
9. Campurile statice sunt mai rapide decat campurile de instanta
10. Metodele statice sunt mai rapide decat metodele de instanta
11. Metode inline
12. Folositi switch in loc de if/else
13. Bucla for mai rapida de regula decat foreach
14. Marcati clasele ca sealed
15. Minimizati declansarea de exceptii
16. Evitati boxing-ul si unboxing-ul
17. Folositi multimile (arrays) in locul colectiilor

WPF
1. Corectati erorile de binding
2. Folositi StaticResource in detrimentul DynamicResource
3. Apelati metoda Freeze pe obiecte de tip Freezable
4.


.NET / C#

1. Folositi clasa Stopwatch pentru a masura performantele

Clasa Stopwatch din namespace-ul System.Diagnostics pune la dispozitia dezvoltatorilor un set de metode si proprietati, care furnizeaza o acuratete mult mai mare asupra timpului scurs decat clasa DataTime sau celebra proprietate statica System.Environment.TickCount.
Prezint mai jos o clasa care automatizeaza procesul de masurare a performantei unei metode.

public sealed class AutoStopwatch : Stopwatch, IDisposable
{
    public AutoStopwatch()
    {
        StartWatching();
    }

    public void Dispose()
    {
        StopWatching();
    }

    [Conditional("DEBUG")]
    private void StartWatching()
    {
        Start();
    }

    [Conditional("DEBUG")]
    private void StopWatching()
    {
        Stop();

        Debug.WriteLine("{0} => elapsed: {0:00}:{1:00}:{2:00}.{3:0000}",
                                new StackTrace(true).GetFrame(2),
                                Elapsed.Hours, Elapsed.Minutes, Elapsed.Seconds, Elapsed.Milliseconds);
    }
}

Metodele StartWatching si StopWatching vor fi compilate si apelate doar in configuratie DEBUG datorita atributului Conditional("DEBUG") aplicat metodelor.
Clasa prezentata poate fi folosita astfel:

void MeasuredMethod()
{
    using (new AutoStopwatch())
    {
        // apeluri de metode si cod
    }
}


2. Concatenarea obiectelor String e costisitoare

Deoarece obiectele de tip string sunt imutabile, adica starea lor interna nu se modifica, se recomanda sa se evite concatenarea acestora. Sa urmarim urmatorul exemplu:

string s1 = "Hello";
s1 += " beautiful";
s1 += " world!";

In prima linie de cod am creat un obiect de tip string si l-am initializat cu textul "Hello". In cea de-a doua linie, am concatenat textul " beautiful". Deoarece s1 este un obiect imutabil, starea lui interna va contine intotdeauna textul cu care a fost initializat, deci, s1 nu se va actualiza cu valoarea "Hello beautiful", ci se va crea un nou obiect string, care va contine textul "Hello beautiful". Procedand analog, se va crea un al treilea obiect, s3, care va contine textul "Hello beautiful world!" iar in final, s1 va contine o referinta spre acest nou obiect. Acest lucru este transparent programatorului, fiind realizat de compilatorul C#.
Codul de mai sus este echivalent cu urmatorul cod:

string s1 = "Hello";
string s2 = s1 + " beautiful";
string s3 = s2 + " world!";
s1 = s3;

Pentru a se evita asemenea situatii, s-a introdus clasa StringBuilder. Aceasta clasa nu mai creaza intern obiecte temporale de tip string, care sa memoreze rezultatul partial al concatenarii, ci adauga textul concatenat unui stream intern. Metoda ToString a clasei StringBuilder creaza un singur obiect de tip string in care se salveaza rezultatul concatenarii. Utilizand aceasta clasa, exemplul de mai sus devine:

var sb = new StringBuilder("Hello");
sb.Append(" beautiful");
sb.Append(" world!");
string s1 = sb.ToString();

O alta abordare, si mai simpla, recurge la metoda statica Format a clasei string, care face acelasi lucru ca StringBuilder. Astfel, codul devine si mai simplu:

string s1 = string.Format("Hello {0} {1}", " beautiful", " world!");


3. String.Equals de preferat operatorului ==

Strict referitor la clasa String, metoda Equals si operatorul == intorc intotdeauna acelasi rezultat. Diferenta dintre cele doua consta in faprul ca operatorul == este strongly typed, adica opereaza doar cu obiecte de tip string in timp ce metoda Equals este suprascrisa furnizand mai multe variante. Performantele in cele doua cazuri variaza in functie de tipul parametrilor.

4. 'as' si 'is' versus casting

Este de preferat sa se foloseasca operatorul as sau is decat sa se recurga la conversia de tip (type casting) deoarece cei doi operatori nu arunca exceptii pe cand type casting-ul arunca exceptii de tipul InvalidCastException. Aceste e un motiv suficient ca sa se foloseasca mai degraba cei doi operatori, fiind cunoscut faptul ca aruncarea si interceptarea unei exceptii reprezinta un mecanism costisitor din punct de vedere al performantei. Sa vedem acum cele doua variante transpuse in cod, prima varianta recurgand la operatorul as.

object o = GetSomeObject();

MyType t = o as MyType;
if (t != null)
{
    // foloseste in continuare 't' de tipul MyType
}
else
{
    // raporteaza eroarea
}

A doua varianta foloseste type casting (conversia de tip):

object o = GetSomeObject();

try
{
    MyType t = (MyType)o;
    // foloseste in continuare 't' de tipul MyType
}
catch (InvalidCastException)
{
    // raporteaza eroarea de conversie
}

Se observa ca varianta in care se foloseste operatorul as este mai lizibila si mai clara conceptual. Un plus de performanta vine se din faptul ca operatorii as so is nu creeaza obiecte noi la runtime, pe cand type casting-ul apeleaza operatorii de conversie, fie ei impliciti sau suprascrisi, acestia creand de regula obiecte tinta noi.


5. Minimizati numarul de parametrii ai unei metode

La apelarea unei metode, runtime-ul va copia parametrii metodei intr-o zona de memorie a stivei destinata parametriilor metodei. Aceste operatii de copiere sunt relativ costisitoare, recomandandu-se definirea metodelor cu un numar de parametrii cat mai mic.


6. Minimizati numarul de variabile locale

La apelarea unei metode, runtime-ul aloca memorie pe stiva programului pentru toate variabilele locale definite in interiorul metodei, indiferent daca acestea sunt folosite sau nu. Din acest motiv, o metoda se apeleaza mai rapid daca contine un numar mai mic de variabile locale. Din acest motiv se recomanda minimizarea numarului de variabile locale sau chiar reciclarea lor, adica folosirea unei variabile in diferite contexte. O modalitate de a realiza cele descrise mai sus consta in inmagazinarea codului mai rar folosit in metode separate.


7. readonly versus const

In .NET exista doua tipuri de constante: runtime si compile-time. Constantele runtime sunt identificate de cuvantul cheie readonly pe cand cele compile-time de cuvantul cheie const. Evident, constantele compile-time sunt mai rapide decat cele runtime deoarece evaluarea lor se face la compilare, nu la rulare. Valoarea constantelor compile-time este inserata direct in cod in locul in care este folosita aceasta constanta, evitandu-se astfel accesarea unei zone de memorie aditionala, in care sunt alocate alte variabile. La constantele de tip runtime problema se schimba, ele fiind evaluate la rulare iar lor li se aloca o zona de memorie, facand astfel accesarea lor mai lenta decat in cazul constantelor de tip compile-time.
O alta deosebire consta in faptul ca o constanta de tip compile-time poate fi declarata si-n interiorul unei metode, lucru nepermis in cazul constantelor de tip runtime. Ce se intampla in momentul in care am definit o constanta de tip compile-time intr-un modul si o folosim in mai multe module, iar dupa o vreme lansam o noua distributie a modulului in care e definita respectiva constanta, alocandu-i acesteie o noua valoare? Dupa cum am amintit mai devreme, valoarea constantelor de tip compile-time este inserata direct in locul in care este folosita. Datorita acestui aspect, in cazul de fata va trebui sa compilam toate modulele in care este folosita respectiva constanta pentru a folosi valoarea cea noua atribuita constantei in noua distributie.
In astfel de cazuri se recomanda mai degraba folosirea constantelor de tip runtime, astfel modulele care folosesc astfel de constante nu vor trebui recompilate.


8. Folositi ConditionalAttribute in locul directivei #if

ConditionalAttribute se aplica la nivel de metoda iar directiva #if la nivel de linie de cod.


9. Campurile statice sunt mai rapide decat campurile de instanta

Accesarea unui camp static (static field) nu presupune identificarea instantei obiectului de tipul clasei din care face parte campul deoarece campurile statice nu sunt asociate cu o instanta a unui obiect. Campurile de tip instanta (instance fields) insa, sunt asociate cu instantele clasei din care fac parte iar accesarea unui astfel de camp presupune mai intai identificarea instantei obiectului din care face parte, ceea ce presupune instructiuni in plus.


10. Metodele statice sunt mai rapide decat metodele de instanta

Campurile statice sunt mai rapide decat campurile de instanta din aceleasi motive expuse la punctul anterior.


11. Metode inline

Foarte adesea, compilatorul C# extrage codul unei metode care contine putine linii de cod si-l insereaza direct in locul apelului metodei, obtinandu-se astfel o performanta mai buna prin eliminarea unui apel de metoda. Din pacate nu avem posibilitatea sa fortam acest lucru prin instruirea compilatorului in vreun fel.
In .NET Framework 4.5 avem insa posibilitatea sa sugeram compilatorului C# sa incerce inlining-ul unei metode prin folosirea atributului MethodImpl (clasa MethodImplAttribute) setat cu valoarea MethodImplOptions.AggressiveInlining. Acest lucru nu garanteaza insa ca compilatorul se va conforma si va realiza acest lucru.

[MethodImpl(MethodImplOptions.AggressiveInlining)]
static int SomeMethod()
{
    int result;
    ...
    // operatii pe variabila 'result'
    ...
    return result;
}


12. Folositi switch in loc de if/else

Declaratiile switch sunt de regula mai rapide decat cele if/else, in special in cazul tipurilor valoare (value types).


13. Bucla for mai rapida de regula decat foreach

In cele mai multe cazuri, bucla for este mai rapida decat foreach si datorita faptului ca-n cazul foreach compilatorul genereaza cateva variabile locale care sunt alocate pe stiva, alocare care costa timp. Acest lucru il puteti urmari in codul IL (Intermediate Language).


14. Marcati clasele ca sealed

Clasele pe care nu le mai derivati marcati-le ca sealed. Astfel, .NET Framework elimina trasaturile clasei care tin de mostenire, producand astfel mai multe optimizari la runtime.


15. Minimizati declansarea de exceptii

Se recomanda evitarea interceptarii si declansarii de exceptii datorita faptului ca aceste operatii sunt foarte costisitoare. Blocurile try/catch n-ar trebui niciodata folosite pentru interceptarea de erori cauzate de accesarea unui obiect nul (NullReferenceException). In loc, folositi cod care sa testeze daca obiectele sunt nule inainte de a le folosi:

if (myObj != null)
{
    // apeleaza alte operatii pe myObj
}
else
{
    // trateaza eroarea
}


16. Evitati boxing-ul si unboxing-ul

Boxing-ul si unboxing-ul permit unui tip valoare sa fie tratat ca un tip referinta, care este stocat in memoria heap. Boxing-ul reprezinta mecanismul de conversie a unui tip valoare intr-un tip referinta iar unboxing-ul este mecanismul invers, de extragere a unui tip valoare dintr-un tip referinta. Aceste doua mecanisme se realizeaza cu costuri mari de performanta datorita conversiei din tipul valoare in tipul referinta si viceversa. Aceasta conversie se realizeaza prin realizarea unei copii a tipului valoare in memoria heap, nascandu-se astfel un nou obiect de tip referinta.
Boxing-ul/Unboxing-ul ar trebui evitat oriunde este posibil datorita costurilor mari de performanta.

Exemple:
int i = 123; // valoarea 123 este stocata pe stiva
object o = i; // boxing
int j = (int)o; // unboxing

Mai multe despre boxing/unboxing gasiti aici.


17. Folositi multimile (arrays) in locul colectiilor

Oriunde este posibil, folositi multimiile de date (arrays) in locul colectiilor, deoarece sunt mai eficiente, in special in cazul tipurilor valoare (value types).
Exemplu:
int[] intArr = new int[10];
// in loc de
IList<int> intList = new List<int>();

Deasemenea, initializati colectiile cu dimensiunea dorita, daca aceasta este cunoscuta.
Exemplu:
IList<int> intList = new List<int>(10);

Colectiile isi aloca implicit o zona de memorie mai mare decat cea destinata unui singur element. Prin adaugare de elemente, cand se atinge dimensiunea alocata, se face o noua alocare samd. Deci, nu se aloca memorie la fiecare adaugare de elemente noi, ci doar cand s-a atins dimensiunea alocata. Alocarile de memorie sunt procese costisitoare iar daca se pot evita astfel de alocari numeroase prin specificarea unei dimensiuni initiale se recomanda sa se recurga la astfel de practici.
Daca se lucreaza cu colectii, se recomanda sa se foloseasca colectii strongly typed pentru a se evita boxing-ul/unboxing-ul, deci, folositi clasa List<T> in locul clasei List, clasa Stack<T> in locul clasei Stack, etc. Deasemenea, folositi multimi strongly typed.
Exemplu:
int[] intArr = new int[10];
intArr[0] = 5; // OK, no boxing overhead
...
object[] intArr = new object[10];
intArr[0] = 5; // NOT OK => boxing


18.


WPF

1. Corectati erorile de binding

Erorile de binding sunt foarte costisitoare din punct de vedere al timpului datorita faptului ca aplicatia logheaza aceste erori in trace log. Acest lucru se poate observa foarte bine daca folosim controale de tip ItmesControls (ListBox, ListView, DataGrid, etc.), al caror elemente sunt conectate defectuos prin binding de colectii relativ mari. Vom observa ca ferestra se incarca greoi iar in modul DEBUG se logheaza erorile de binding in fereastra Output.


2. Folositi StaticResource in detrimentul DynamicResource

Accesul la o resursa de tip DynamicResource este mai lent decat la una de tip StaticResource.


3. Apelati metoda Freeze pe obiecte de tip Freezable

Apelatand metoda Freeze pe obiecte de tip Freezable se reduce consumul de memorie si se imbunatateste performantele acestor obiecte, deoarece astfel nu se vor mai monitoariza de catre runtime pentru schimbari.
Spre exemplu, obiectele de tip SolidColorBrush au la baza tipul Freezable. Implicit, runtime-ul genereaza intern in aceste obiecte niste delegate, care monitorizeaza aceste obiecte la schimbari ale paletei de culori. Daca nu dorim sa schimbam la runtime paleta de culori a unui astfel de obiect, apelam metoda Freeze iar astfel nu se mai genereaza codul pentru monitorizarea obiectelor. Astfel, aceste obiecte ocupa mai putin loc in memorie si nu mai verifica daca s-a schimbat paleta de colori.
In XAML, acest lucru se realizeaza prin declararea namespace-ului xmlns:presentationOptions="http://schemas.microsoft.com/winfx/2006/xaml/presentation/options" iar in obiectul tinta se seteaza atributul presentationOptions:Freeze="True":

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:presentationOptions="http://schemas.microsoft.com/winfx/2006/xaml/presentation/options"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

    <SolidColorBrush x:Key="SomeBrush" Color="#FFF8BC5C" presentationOptions:Freeze="True"/>

</ResourceDictionary>

4.


Niciun comentariu:

Trimiteți un comentariu