Optimizarea programelor

Optimizarea poate avea loc la mai multe niveluri. De obicei, nivelurile superioare au un impact mai mare și sunt mai greu de modificat ulterior în cadrul unui proiect, necesitând modificări semnificative sau o rescriere completă dacă trebuie modificate. Astfel, optimizarea poate proceda de obicei prin rafinare de la un nivel superior la unul inferior, câștigurile inițiale fiind mai mari și obținute cu mai puțină muncă, iar câștigurile ulterioare fiind mai mici și necesitând mai multă muncă. Cu toate acestea, în unele cazuri, performanța globală depinde de performanța unor porțiuni de nivel foarte scăzut ale unui program, iar schimbările mici într-o etapă târzie sau luarea în considerare timpurie a detaliilor de nivel scăzut pot avea un impact foarte mare. În mod obișnuit, se acordă o anumită atenție eficienței pe parcursul unui proiect – deși acest lucru variază în mod semnificativ – dar optimizarea majoră este adesea considerată o perfecționare care se face târziu, dacă se face vreodată. În cazul proiectelor cu o durată mai lungă de desfășurare, există de obicei cicluri de optimizare, în care îmbunătățirea unui domeniu dezvăluie limitări în altul, iar acestea sunt de obicei reduse atunci când performanța este acceptabilă sau când câștigurile devin prea mici sau costisitoare.

Deoarece performanța face parte din specificațiile unui program – un program care este neobișnuit de lent nu este adecvat scopului: un joc video cu 60 Hz (cadre pe secundă) este acceptabil, dar 6 cadre pe secundă este inacceptabil de agitat – performanța este un aspect luat în considerare de la început, pentru a se asigura că sistemul este capabil să ofere o performanță suficientă, iar prototipurile timpurii trebuie să aibă o performanță aproximativ acceptabilă pentru a exista încredere că sistemul final va atinge (cu optimizare) o performanță acceptabilă. Acest aspect este uneori omis în convingerea că optimizarea poate fi întotdeauna făcută mai târziu, ceea ce are ca rezultat prototipuri de sisteme mult prea lente – adesea cu un ordin de mărime sau mai mult – și sisteme care, în cele din urmă, sunt eșecuri deoarece din punct de vedere arhitectural nu-și pot atinge obiectivele de performanță, cum ar fi Intel 432 (1981); sau sisteme care necesită ani de muncă pentru a atinge o performanță acceptabilă, cum ar fi Java (1995), care a atins o performanță acceptabilă doar cu HotSpot (1999). Gradul în care performanța se modifică între prototip și sistemul de producție și cât de ușor de optimizat este aceasta, poate fi o sursă semnificativă de incertitudine și risc.

Nivelul de proiectareEdit

La cel mai înalt nivel, proiectarea poate fi optimizată pentru a utiliza cât mai bine resursele disponibile, având în vedere obiectivele, constrângerile și utilizarea/încărcarea preconizată. Proiectarea arhitecturală a unui sistem afectează în mod covârșitor performanța acestuia. De exemplu, un sistem care este legat de latența rețelei (unde latența rețelei este principala constrângere asupra performanței globale) ar fi optimizat pentru a minimiza călătoriile în rețea, în mod ideal făcând o singură cerere (sau nicio cerere, ca într-un protocol push) mai degrabă decât mai multe călătorii dus-întors. Alegerea designului depinde de obiective: atunci când se proiectează un compilator, dacă compilarea rapidă este prioritatea cheie, un compilator cu un singur pas este mai rapid decât un compilator cu mai mulți pași (presupunând aceeași muncă), dar dacă viteza codului de ieșire este obiectivul, un compilator cu mai mulți pași, mai lent, îndeplinește mai bine obiectivul, chiar dacă durează mai mult timp. Alegerea platformei și a limbajului de programare are loc la acest nivel, iar schimbarea acestora necesită frecvent o rescriere completă, deși un sistem modular poate permite rescrierea doar a unor componente – de exemplu, un program Python poate rescrie secțiunile critice din punct de vedere al performanței în C. Într-un sistem distribuit, alegerea arhitecturii (client-server, peer-to-peer etc.) are loc la nivel de proiectare și poate fi dificil de schimbat, în special dacă toate componentele nu pot fi înlocuite în sincronizare (de ex, clienți vechi).

Algoritmi și structuri de dateEdit

După o proiectare generală, urmează o bună alegere a algoritmilor și structurilor de date eficiente, precum și o implementare eficientă a acestor algoritmi și structuri de date. După proiectare, alegerea algoritmilor și a structurilor de date afectează eficiența mai mult decât orice alt aspect al programului. În general, structurile de date sunt mai dificil de modificat decât algoritmii, deoarece o ipoteză de structură de date și ipotezele de performanță ale acesteia sunt utilizate în tot programul, deși acest lucru poate fi minimizat prin utilizarea unor tipuri de date abstracte în definițiile funcțiilor și prin menținerea definițiilor concrete ale structurilor de date limitate la câteva locuri.

Pentru algoritmi, aceasta constă în primul rând în asigurarea faptului că algoritmii sunt constanți O(1), logaritmici O(log n), liniari O(n) sau, în unele cazuri, log-liniari O(n log n) în intrare (atât în spațiu, cât și în timp). Algoritmii cu complexitate pătratică O(n2) nu reușesc să se scaleze și chiar și algoritmii liniari cauzează probleme dacă sunt apelați în mod repetat și sunt de obicei înlocuiți cu cei constanți sau logaritmici, dacă este posibil.

Dincolo de ordinea asimptotică de creștere, factorii constanți contează: un algoritm asimptotic mai lent poate fi mai rapid sau mai mic (pentru că este mai simplu) decât un algoritm asimptotic mai rapid atunci când amândoi se confruntă cu o intrare mică, ceea ce poate fi cazul care apare în realitate. Adesea, un algoritm hibrid va oferi cea mai bună performanță, datorită faptului că acest compromis se schimbă odată cu mărimea.

O tehnică generală de îmbunătățire a performanței este evitarea muncii. Un bun exemplu este utilizarea unei căi rapide pentru cazurile obișnuite, îmbunătățind performanța prin evitarea muncii inutile. De exemplu, utilizarea unui algoritm simplu de dispunere a textului pentru textul latin, trecând la un algoritm complex de dispunere doar pentru scrieri complexe, cum ar fi Devanagari. O altă tehnică importantă este memoria cache, în special memoizarea, care evită calculele redundante. Din cauza importanței memoriei cache, există adesea mai multe niveluri de memorie cache într-un sistem, ceea ce poate cauza probleme din cauza utilizării memoriei și probleme de corectitudine din cauza memoriei cache învechite.

Nivelul codului sursăEdit

Dincolo de algoritmii generali și de implementarea lor pe o mașină abstractă, alegerile concrete la nivelul codului sursă pot face o diferență semnificativă. De exemplu, pe primele compilatoare C, while(1) era mai lent decât for(;;) pentru o buclă necondiționată, deoarece while(1) evalua 1 și apoi avea un salt condițional care testa dacă era adevărat, în timp ce for (;;) avea un salt necondiționat . Unele optimizări (cum ar fi aceasta) pot fi efectuate în prezent de compilatoarele de optimizare. Acest lucru depinde de limbajul sursă, de limbajul mașinii țintă și de compilator și poate fi atât dificil de înțeles sau de prezis, cât și de schimbat în timp; acesta este un loc cheie în care înțelegerea compilatoarelor și a codului mașină poate îmbunătăți performanța. Mișcarea codului în buclă invariantă și optimizarea valorii de retur sunt exemple de optimizări care reduc nevoia de variabile auxiliare și pot duce chiar la o performanță mai rapidă prin evitarea optimizărilor ocolitoare.

Nivelul de compilareEdit

Între nivelul sursă și cel de compilare, directivele și stegulețele de compilare pot fi utilizate pentru a regla opțiunile de performanță în codul sursă și, respectiv, în compilator, cum ar fi utilizarea definițiilor preprocesorului pentru a dezactiva caracteristicile software inutile, optimizarea pentru anumite modele de procesoare sau capacități hardware sau pentru a prezice ramificarea, de exemplu. Sistemele de distribuție de software bazat pe sursă, cum ar fi Ports de la BSD și Portage de la Gentoo, pot profita de această formă de optimizare.

Nivel de compilareEdit

Utilizarea unui compilator de optimizare tinde să asigure că programul executabil este optimizat cel puțin atât cât poate prezice compilatorul.

Nivelul de asamblareEdit

La nivelul cel mai de jos, scrierea codului folosind un limbaj de asamblare, proiectat pentru o anumită platformă hardware poate produce cel mai eficient și compact cod dacă programatorul profită de întregul repertoriu de instrucțiuni de mașină. Din acest motiv, multe sisteme de operare utilizate pe sisteme integrate au fost scrise în mod tradițional în cod de asamblare. Programele (cu excepția celor foarte mici) sunt rareori scrise de la început până la sfârșit în asamblare din cauza timpului și a costurilor implicate. Majoritatea sunt compilate dintr-un limbaj de nivel înalt în asamblare și optimizate manual de acolo. Atunci când eficiența și dimensiunea sunt mai puțin importante, părți mari pot fi scrise într-un limbaj de nivel înalt.

Cu compilatoare de optimizare mai moderne și complexitatea mai mare a procesoarelor recente, este mai greu să scrii un cod mai eficient decât cel generat de compilator și puține proiecte au nevoie de această etapă de optimizare „finală”.

Mult cod scris astăzi este destinat să ruleze pe cât mai multe mașini posibil. În consecință, programatorii și compilatorii nu profită întotdeauna de instrucțiunile mai eficiente oferite de procesoarele mai noi sau de ciudățeniile modelelor mai vechi. În plus, codul de asamblare reglat pentru un anumit procesor fără a utiliza astfel de instrucțiuni ar putea fi în continuare suboptimal pe un procesor diferit, așteptând un reglaj diferit al codului.

Astăzi, în mod obișnuit, mai degrabă decât să scrie în limbaj de asamblare, programatorii vor folosi un dezasamblorator pentru a analiza ieșirea unui compilator și pentru a modifica codul sursă de nivel înalt, astfel încât să poată fi compilat mai eficient sau să înțeleagă de ce este ineficient.

Timp de execuțieEdit

Compilatoarele just-in-time pot produce cod mașină personalizat pe baza datelor din timpul execuției, cu prețul unei supracompilări. Această tehnică datează de la cele mai vechi motoare de expresii regulate și a devenit larg răspândită cu Java HotSpot și V8 pentru JavaScript. În unele cazuri, optimizarea adaptivă poate fi capabilă să realizeze o optimizare în timp de execuție care să depășească capacitatea compilatoarelor statice prin ajustarea dinamică a parametrilor în funcție de datele de intrare reale sau de alți factori.

Optimizarea ghidată de profil este o tehnică de optimizare a compilării înainte de timp (AOT) bazată pe profiluri în timp de execuție și este similară cu un analog static de „caz mediu” al tehnicii dinamice de optimizare adaptivă.

Codul auto-modificator se poate modifica singur ca răspuns la condițiile de timp de execuție pentru a optimiza codul; acest lucru a fost mai frecvent în programele în limbaj de asamblare.

Câteva modele de procesoare pot efectua unele optimizări în timp de execuție. Câteva exemple includ execuția în afara ordinii, execuția speculativă, conductele de instrucțiuni și predictorii de ramificare. Compilatoarele pot ajuta programul să profite de aceste caracteristici ale procesorului, de exemplu prin programarea instrucțiunilor.

Optimizări dependente și independente de platformăEdit

Optimizarea codului poate fi, de asemenea, clasificată în linii mari în tehnici dependente și independente de platformă. În timp ce acestea din urmă sunt eficiente pe majoritatea sau pe toate platformele, tehnicile dependente de platformă utilizează proprietăți specifice ale unei platforme sau se bazează pe parametri care depind de o singură platformă sau chiar de un singur procesor. Prin urmare, ar putea fi necesară scrierea sau producerea de versiuni diferite ale aceluiași cod pentru procesoare diferite. De exemplu, în cazul optimizării la nivel de compilare, tehnicile independente de platformă sunt tehnici generice (cum ar fi derularea buclelor, reducerea numărului de apeluri de funcții, rutine eficiente din punct de vedere al memoriei, reducerea condițiilor etc.), care au un impact similar asupra majorității arhitecturilor CPU. Un exemplu excelent de optimizare independentă de platformă a fost prezentat în cazul buclei for interne, unde s-a observat că o buclă cu o buclă for internă efectuează mai multe calcule pe unitate de timp decât o buclă fără aceasta sau una cu o buclă while internă. În general, acestea servesc la reducerea lungimii totale a traseului de instrucțiuni necesar pentru a finaliza programul și/sau la reducerea utilizării totale a memoriei în timpul procesului. Pe de altă parte, tehnicile dependente de platformă implică programarea instrucțiunilor, paralelismul la nivel de instrucțiuni, paralelismul la nivel de date, tehnicile de optimizare a cache-ului (adică parametrii care diferă între diverse platforme), iar programarea optimă a instrucțiunilor ar putea fi diferită chiar și pe procesoare diferite de aceeași arhitectură.

.