RicH and FamouS

       Home         Glosar IT                                                                                                                                                                                                              SUBSCRIBE NOW!
        

03.04.2009

Optimizarea - aliat sau dusman?

Când este necesară optimizarea
Un programator versat nu va scrie niciodată cod inefficient. Cel puţin nu în mod intenţionat. Optimizarea îşi are rostul când performanţa trebuie să fie maximă. În momentul în care se doreşte o optimizare a codului, trebuie studiat foarte atent locul în care are loc gâtuirea. Optimizarea se poate face în mai multe locuri – la prelucrarea imaginilor (acestea se încarcă prea încet şi/sau consumă excesiv de multă memorie), la accesul la date (viteza de accesare şi prelucrare a datelor este prea mică), asupra unor algoritmi (implementaţi necorespunzător), etc.

La capitolul optimizarea algoritmilor proprii implementaţi, se întâlnesc de obicei 3 mari cazuri:
- Se ignoră în prima fază că tocmai propriul algoritm este cel care cauzează gâtuirea, acest lucru devenind periculos deoarece se evită studirea problemei tocmai în locul în care aceasta există.
- Se determină algoritmul care cauzează gâtuirea şi se rescrie complet acest algoritm.
- Se determină algoritmul care cauzează gâtuirea dar mai întâi se testează impactul global pe care această gâtuirea o are asupra întregii aplicaţii, înainte de modificarea sau rescrierea lui.

Această ultimă abordare este cea mai indicată, deoarece uneori gâtuirea generată de un algoritm poate să aibă impact nememnificativ asupra sistemului în raport cu efortul de rescriere a întregului algoritm. Uneori, rescrierea întregului algoritm, mai ales în cazul algoritmilor complexi poate fi destul de dificilă, cauzând întârzieri pentru întregul proiect şi costuri suplimentare pentru firmă.

În momentul în care se testează o aplicaţie, în vederea determinării gâtuirilor, se folosesc o serie de utilitare care măsoare timpul în care aplicaţia execută o anume funcţie. Aceste unelte sunt însă cu două tăişuri.
Unele din ele par să nu ţină cont că, în special sub sistemele Windows, în timpul în care se execută o anume funcţie a programului testat rulează şi thread-uri ale altor programe de sistem. De exemplu, aplicaţia noastră rulează în spaţiul utilizator. Dar tot în acest timp, în paralel lucrează şi kernelul sistemului de operare.
Alte unelte mai performante, ţin cont de toate acestea şi tratează ca atare timpul consumat de kernel, oferind o imagine cât de cât reală a timpului în care se execută efectiv aplicaţia.

În unele cazuri, din aplicaţia noastră este apelat kernelul pentru a executa anumite operaţii, deci timpul în care rulează acest kernel este foarte important şi nu trebuie ignorat. De la versiunea 4.0 a Windows NT, GDI-ul nu mai este un proces care rulează pe nivelul utilizator, ci este integrat în kernel.
În concluzie, la întrebare Ce să optimizăm?, se poate răspunde uşor: acele părţi ale programului care consumă timp excesiv. Dar şi aici apare o problemă. O optimizare locală, unde se ignoră performanţele globale ale aplicaţiei este inutilă.

Aici pot să dau un exemplu foarte realist. Să luăm de exemplu Visual Basic 5.0. Acesta venea în plus cu un compilator nativ, pe lângă interpretor. Problema a fost că de un plus de viteză au beneficiat numai aplicaţiile care foloseau calcule matematice intensiv. În cazul unei aplicaţii de baze de date, unde se foloseau mult tehnologii externe – MDAC, componente ActiveX, apeluri de funcţii API, nu se prea sesiza vreo diferenţă.
De ce? Pentru că, codul compilat al programului conţinea în mare parte apeluri de funcţii API, apeluri de metode şi proprietăţi ale obiectelor COM/DCOM/ActiveX, iar timpul consumat pentru aceste apeluri era infim faţă de timpul în care acest funcţii API, metode şi proprietăţi ale obiectelor COM/DCOM rulau efectiv.
Sfaturi privind optimizarea

Chiar dacă compilatoarele moderne de C++ sunt foarte capabile şi permit optimizarea aplicaţiilor d-voastră, unele intervenţii în cod sunt încă necesare. Astfe, voi discuta mai departe aspecte generale ale optimizării programelor scrise în acest limbaj de programare.
O tehnică pe care trebuie să o aveţi în vedere este delegarea către sistemul de operare. Delegarea permite folosirea codului deja existent, care a fost testat şi optimizat pentru obţinerea unei performanţe cât mai mari.

Sistemul de operare de multe ori oferă aşa numitele hook-uri (cârlige) la hardware. Este cazul acceleratoarelor 3D şi a plăcilor de reţea cu checksum hardware. Aproape întotdeauna, folosirea funcţiilor API implementate în hardware poate duce la performanţe mult mai bune decât dacă aţi scrie d-voastră cod pentru implementarea acestor funcţii.
În cazul plăcilor grafice, cu accelerare 3D, emularea software produce rezultate sensibil mai slabe decât folosirea funcţiilor API dedicate implementate hardware în acceleratorul 3D.

Alt exemplu este tehnologia OLE DB pentru accesul la bazele de date. Această tehnologie este matură, testată şi optimizată şi este de preferat să o folosiţi în loc să vă definiţi propriul motor de acces la bazele de date.
De exemplu SQL Server este dedicat pentru execuţia de instrucţiuni SQL, şi va face această treabă mult mai repede decât aţi face-o d-voastră prin propriile rutine, indiferent de limbajul de programare folosit. O altă prolemă importantă pe care trebuie să o luaţi în considerare este sistemul de operare folosit. Windows NT şi succesorii lui merg mai rapid şi mai stabil decât sistemele de operare Windows pe tehnologie 9x. Deci, dacă se poate, ţintiţi sisteme de operare pe tehnologie NT, mai ales pentru serverele de aplicaţii şi pentru sistemele de acces la bazele de date.

Algoritmii pe care îi folosiţi în aplicaţia d-voastră pot să influenţeze foarte mult performanţa de ansamblu a aplicaţiei. Folosiţi cel mai bun algoritm care vă poate rezlva o anumită problemă. Implementaţi algoritmii şi în funcţie de cantitatea de date pe care respectivul algoritm o procesează.
Alegeţi cel mai bun raport între uşurinţa de implementare a algoritmului şi performanţele acestuia.

Pentru un set mic de date, a implementa un algoritm complex poate fi pierdere de timp. Luaţi în considerare tot timpul factorul timp şi costurile pe care implementarea unui anumit algoritm le implică. Nu veţi justifica costurile suplimentare sau întârzierea lansării aplicaţiei pentru un algoritm foarte complex, aplicat unui set mic de date, care nu produce performanţe vizibile mai mari decât un alt algoritm nu atât de complicat, dar mai rapid de implementat.

Biblioteca STL de obicei este însoţită de implementarea algoritmilor standard, deci puteţi folosi această bibliotecă pentru anumite scopuri. În acest fel aveţi la îndemână algoritmi de calitate deja implementaţi. Aici trebuie să aveţi grijă, pentru că unele implementări sunt foarte generice.
De obicei compilatorul de Visual C++ este capabil să recunoască de exemplu constantele dintr-o buclă şi să le scoată în afară, dar nu îi este clar care apeluri de funcţii sau metode sunt constante. Ori de câte ori aveţi ocazia, folosiţi bucle în care contorul descreşte, deoarece se simplifică în acest fel generarea codului pe măsură ce contorul scade, urmat de un salt dacă nu este zero. (în ASM, pentru acest salt se foloseşte instrucţiunea jnz)

Altă tehnică este folosirea operatorului ++ sau - -, de incrementare, respectiv decrementare, înaintea variabilei.
Ex, este mai bine să scrieţi:
Cod:
++ nVariabila,

sau
Cod:
-- nVariabila


decât:
Cod:
nVariabila ++,

sau
Cod:
nVariabila --


Această tehnică este utilă mai ales în cazul procesoarelor Pentium, unde incrementarea nu se poate face până când comparaţia nu este completă. Nucleul procesoarelor Pentium pot folosi register antialiasing pentru a începe incrementarea, folosind „load effective address”, pentru a nu ruina testul de comparaţie.

Exemple:

Pentru linia:
Cod:
for(i=0;i

Procesorul execută:
Cod:
inc ecx
cmp ecx, limit
jl loop_start


Pentru linia:
Cod:
for(i=limit+1;i>0;--i)

Procesorul execută:
Cod:
dec ecx
jgt loop_start


Pentru linia:
Cod:
for(i=0;i

Procesorul execută:
Cod:
cmp ecx,limit
lea ecx, [ecx+1] ; fără influenţă asupra regiştrilor
jl loop_start


Pentru linia:
Cod:
for(i=limit+1;i>0;i--)

Procesorul execută:
Cod:
test ecx
lea ecx, [ecx-1]
jl loop_start


Apelurile de funcţii în C/C++ - cum afectează acestea performanţa globală a aplicaţiei.

1. Funcţiile C
Apelul este simplu: pune argumentul pe stack, excută o instrucţie CALL şi apoi RET la sfârşitul apelului, după care extrage argumentul.
Apelurile de funcţii __fastcall sunt mai rapide pentru unul sau două argumente, deoarece aceştia sunt pasaţi în regiştrii.
2. Funcţii C++ nonvirtuale
Nu sunt mai lente decât funcţiile C, dar toate funcţiile au pointerul this, aşa că exită întotdeauna un argument pe care să-l pună sau să-l extragă (push/pop).
3. Membrii virtuali ai funcţiilor C++
Acestia ar trebui evitaţi de tot, în cazul în care este posibil.
4. Apeluri de funcţii din obiecte COM In Process
Presupunând că interfaţa COM poate fi accesată direct din threadul care o apelează, apelurile de funcţii COM nu au impact mai mare decât apelurile de funcţii virtuale, ceea ce şi sunt de fapt. Un alt aspect este că marshalingul parametrilor poate fi mai mare consumator de timp decât pasarea pointerilor.
5. Apelurile de funcţii API
Orice apel care rulează pe ring 3 (modul utilizator ), are acelaşi impact ca şi oricare alt apel al unei funcţii dintr-un fişier .DLL. Problema stă altfel dacă apelul se face de pe ring 0 (modul kernel ), iar un apel care forţează o tranziţie de ring necesită minim 600 de cicluri de tact.
Aliniamentul elementelor în structuri de date
Într-o structură, încercaţi să aliniaţi toate elementele la 4 sau 8 bytes. Pentru Visual C++ 6.0, prestabilit este 8 bytes, însă 4 bytes sunt la fel de buni pentru valorile pe 32 de biţi.
Aliniamentul pe 8 bytes este indicat pentru structuri cum ar fi fişierele sau structurile de reţea, însă aceste înregistrări ar trebui să folosească întregi pe 64 de biţi. Pentru orice altceva, rămâneţi la aliniamentul pe 4 bytes.

Software Pipelining
Această tehnică pipeline a fost implementată în procesoarele Merced de la Intel, aşa că, pentru a beneficia de avantajele pe care le oferă, este nevoie de compilatoare care să ştie să o implementeze în software.
Software pipelining este un concept de construcţie a buclelor în care fiecare iteraţie lucrează la o parte diferită din task. De exemplu la procesarea pipeline a imaginilor, un pixel poate fi transformat în timp ce altul este scris pe disk.
Această tehnică este bine să o lăsaţi în grija compilatorului, cu excepţia cazului în care lucraţi direct cu instrucţiuni în limbaj de asamblare.

Când să nu optimizaţi
Nu încercaţi să faceţi optimizări care nu au sens. De exemplu, unii încearcă să optimizeze interfaţa grafică (GUI), algoritmii de baze de date, etc. Nu pierdeţi timpul să creaţi de exemplu propriul motor de acces la bazele de date, decât dacă aveţi un motiv foarte serios. Motoare ca JET, ADO, DAO sunt implementate, optimizate şi testate de echipe întregi de programatori profesionişti. Deci folosiţi cât mai mult ceea ce deja există şi merge bine.

De exemplu, de ce nu este eficient să optimizăm update-ul meniurilor sau controalelor dintr-o interfaţă grafică? Păi hai să luăm în considerare şi factorul uman. O relatare interesantă am găsit-o într-un articol pe internet:

Utilizatorul execută un click cu mouse-ul. Să spunem că mouse-ul se află la aproximativ un metru de ureche. Se ştie că sunetul se propagă cu o viteză de 380 m/sec.
Asta înseamnă că este nevoie de aproximativ 2 ms pentru ca sunetul click-ului de mouse sau apăsării de tastă să ajungă la urechea umană.
Calea neurală de la vârful degetului până la creier este de aproximativ 1 m. Propagarea impulsului nervos este de aproximativ 100 m/sec, asta însemnând că senzaţia executării unui click sau a unei apăsări de tastă are nevoie de 10 ms pentru a ajunge la creier. Durata de percepţie în creier variază între 50 şi 250 ms sau chiar mai mult.
Deci, câte instrucţiuni de procesor Pentium se execută în 2 ms? Dar în 10 ms? Dar în 250 ms? În 2 ms, într-un procesor Pentium !!!/500Mhz au loc aproximativ 1.000.000 cicluri de ceas, timp în care se execută o mulţime de instrucţiuni!

Foarte interesant acest mod de abordare a problemei, nu-i aşa?
Toate acestea nu au împiedicat corporaţia Microsoft pentru a viola modelul obiectual al C++ referitor la tratarea mesajelor.
La apelul CWnd::OnCeva(...), în loc de a se apela funcţia DefWindowProc cu parametrii care îi specificaţi d-voastră, se refoloseşte parametrul ultimului mesaj pentru a apela ::DefWindowProc.
Aceasta are ca scop final, după spusele lui Microsoft, de a reduce dimensiunea librăriei runtime MFC. Acest lucru este un exemplu de optimizare care nu-şi are rostul, pentru că într-un DLL imens, care conţine mii de instrucţiuni, cum este runtime-ul MFC, micşorarea bibliotecii prin această tehnică este atât insesizabilă cât şi fără impact real asupra vitezei de rulare.

Concluzie
Optimizarea contează numai atunci când este nevoie de ea. Când este nevoie de ea, contează foarte mult, dar când nu este necesară, nu pierdeţi timp şi efort pentru a o implementa. Chiar dacă aţi ajuns la concluzia că este necesară, trebuie să studiaţi atent în ce loc anume este necesară. Toate acestea se fac testând cu date concrete aplicaţia d-voastră, în diferite situaţii. Dacă nu aţi definit concret partea aplicaţiei care trebuie optimizată, s-ar putea să optimizaţi ceea ce nu trebuie. Rezultatul acestui lucru va fi un cod obscur, greu de întreţinut şi depanat şi nu vă va rezolva adevărata problemă. Asta să nu mai vorbim de timpul şi de costurile suplimentare pe care toate acestea le implică.

Deci, atenţie ce optimizaţi şi mai ales când optimizaţi. În funcţie de capacitatea d-voastră de a identifica realele gâtuiri, optimizarea vă poate fi duşman sau aliat. Mult succes!

    Blog din Moldova    FastCounter 

 
Copyright © 2008-2010 Foster1. All rights reserved.