Optimizarea compilatorului

Un compilator de optimizare  este un compilator care folosește diverse metode pentru a obține un cod de program mai optim, menținând în același timp funcționalitatea acestuia. Cele mai comune obiective de optimizare sunt: ​​reducerea timpului de execuție a programului, creșterea performanței, compactarea codului programului, economisirea memoriei, minimizarea costurilor energetice, reducerea numărului de operațiuni I/O .

Optimizarea poate apărea implicit în timpul traducerii unui program, dar este în general considerată un pas separat în munca compilatorului. De asemenea, linkerii pot efectua o parte din optimizări, cum ar fi eliminarea subrutinelor neutilizate sau reordonarea acestora .

Optimizarea este de obicei implementată printr-o serie de transformări de optimizare, algoritmi care preiau un program și îl modifică pentru a produce o variantă echivalentă semantic care este mai eficientă pentru un set de obiective de optimizare. Unele probleme de optimizare a codului s-au dovedit a fi NP-complete [1] , sau chiar indecidabile [2] . Cu toate acestea, practic multe dintre ele sunt rezolvate prin metode euristice, care dau rezultate destul de satisfăcătoare.

Distingeți între optimizarea la nivel scăzut și la nivel înalt. Optimizarea la nivel scăzut transformă programul la nivelul instrucțiunilor elementare, de exemplu, instrucțiuni ale unui procesor cu o anumită arhitectură . Optimizarea la nivel înalt se realizează la nivelul elementelor structurale ale programului, cum ar fi module, funcții, ramuri, cicluri.

Tipuri de optimizări

Metodele utilizate în optimizări pot fi clasificate după sfera de aplicare: pot afecta atât o singură instrucțiune, cât și un întreg program. Metodele locale (care afectează o mică parte a programului) sunt mai ușor de implementat decât metodele globale (se aplică întregului program), dar metodele globale sunt adesea mai benefice.

Optimizare peephole

Optimizările peephole locale iau în considerare mai multe instrucțiuni adiacente (în ceea ce privește unul dintre graficele reprezentării programului) (ca și cum ar fi „privind prin  vizor  ” la cod) pentru a vedea dacă este posibil să se efectueze vreo transformare cu ele în ceea ce privește optimizarea poartă. În special, ele pot fi înlocuite cu o singură instrucțiune sau cu o secvență mai scurtă de instrucțiuni.

De exemplu, dublarea unui număr se poate face mai eficient folosind o schimbare la stânga sau prin adăugarea numărului la același.

Optimizare locală

În optimizarea locală, sunt luate în considerare doar informațiile unui bloc de bază pe pas [3] . Deoarece nu există tranziții ale fluxului de control în blocurile de bază , aceste optimizări necesită puțină analiză (economisirea timpului și reducerea cerințelor de memorie), dar înseamnă, de asemenea, că nu sunt stocate informații pentru următorul pas.

Optimizare intraprocedurală

Optimizările intraprocedurale ( engleză  intraprocedural ) sunt optimizări globale care sunt efectuate în întregime în cadrul unei unități de traducere (de exemplu, funcții sau proceduri) [4] . Cu o astfel de optimizare, sunt implicate mult mai multe informații decât în ​​cea locală, ceea ce vă permite să obțineți efecte mai semnificative, dar acest lucru necesită adesea calcule care consumă mult resurse. Dacă există variabile globale în unitatea de program care este optimizată, optimizarea de acest fel poate fi dificilă.

Optimizarea buclei

Există un număr mare de optimizări aplicate buclelor. Cu un număr mare de iterații ale buclei, astfel de optimizări sunt extrem de eficiente, deoarece o parte semnificativă a execuției programului este afectată de o mică transformare. Deoarece buclele reprezintă o parte semnificativă a timpului de execuție al multor programe, optimizările buclelor există în aproape toate compilatoarele și sunt cele mai importante.

De exemplu, prin identificarea invarianților buclei , uneori este posibil să eliminați unele operații din buclă pentru a nu efectua calcule repetate redundante.

Optimizare interprocedurală

Astfel de tipuri de optimizări analizează simultan întregul cod sursă al programului. Cantitatea mai mare de informații extrase prin aceste metode înseamnă că optimizările pot fi mai eficiente decât alte metode. Astfel de optimizări pot folosi metode destul de complexe, de exemplu, un apel de funcție este înlocuit cu o copie a corpului funcției (inline sau inline).

Exemplu Să fie o funcție:

int pred ( int x ) { dacă ( x == 0 ) returnează 0 ; altfel întoarcere x - 1 ; }

Înainte de a-l încorpora, codul arăta astfel:

int f ( int y ) { return pred ( y ) + pred ( 0 ) + pred ( y + 1 ); }

După optimizare:

int f ( int y ) { int temp = 0 ; dacă ( y == 0 ) temp += 0 ; else temp += y - 1 ; /* (unu) */ dacă ( 0 == 0 ) temp += 0 ; else temp += 0 - 1 ; /* (2) */ dacă ( y + 1 == 0 ) temp += 0 ; else temp += ( y + 1 ) - 1 ; /* (3) */ temperatura de retur ; }

Încorporarea funcției elimină supraîncărcarea asociată apelurilor de funcții. În plus, după inline, este posibil să se aplice optimizări intra-procedurale care au fost imposibil sau prea greu de implementat înainte. Cu toate acestea, inlining-ul are dezavantajele sale, ca aproape orice optimizare - crește dimensiunea statică a codului, ceea ce poate duce la efecte negative în părțile echipamentului care sunt sensibile la acest factor.

Factori care afectează optimizarea

Printre factorii care influențează optimizarea, atât caracteristicile de calcul ale mașinii țintă (de exemplu, numărul și viteza de ceas a nucleelor ​​de procesor, dimensiunea memoriei cache a procesorului , lățimea de bandă a magistralei sistemului , cantitatea de RAM), cât și arhitectura țintei. procesor (în special, în diferite arhitecturi sunt disponibile un număr diferit de registre de uz general, conducta de calcul este implementată diferit ). O altă clasă de factori care afectează optimizarea sunt scenariile de utilizare a codului țintă, de exemplu, țintele de optimizare pot varia semnificativ pentru codul de depanare și testare, rulare ocazională, utilizare continuă, aplicații încorporate sau independente.

Principii generale

Printre principiile de optimizare utilizate în diferite metode de optimizare din compilatoare (unele dintre ele se pot contrazice sau pot fi inaplicabile pentru diferite obiective de optimizare):

  • reducerea redundanței: reutilizarea rezultatelor calculelor, reducerea numărului de recalculări;
  • compactare cod: eliminarea calculelor inutile și a valorilor intermediare;
  • reducerea numărului de tranziții în cod: de exemplu, utilizarea funcțiilor de încorporare ( extinderea în linie engleză  ) sau derularea buclei permite în multe cazuri accelerarea execuției programului cu prețul creșterii dimensiunii codului;
  • localitate: codul și datele care trebuie accesate în viitorul apropiat trebuie plasate unul lângă celălalt în memorie pentru a respecta principiul localității de  referință ;
  • utilizarea unei ierarhii de memorie: plasați datele cele mai frecvent utilizate în registrele de uz general, datele mai puțin utilizate în cache , chiar și datele mai puțin utilizate în RAM și datele cel mai puțin utilizate pe disc .
  • paralelizare: operațiunile de reordonare pot permite efectuarea mai multor calcule în paralel, ceea ce accelerează execuția programului (vezi derularea buclei ).

Metode specifice

Optimizarea buclei

Analiza variabilelor inductive dacă variabila din buclă este rezultatul unei funcții liniare simple a unei variabile inductive, cum ar fi for (i=0; i < 10; ++i) j = 17 * i;, atunci puteți actualiza acea variabilă în mod corespunzător la fiecare iterație. Aceasta se numește scăderea puterii operațiunilor . Împărțirea ciclului în părți Optimizarea încearcă să împartă bucla în mai multe bucle separate cu același interval de index. Fiecare buclă nouă face parte din corpul buclei originale. Acest lucru poate îmbunătăți localitatea referințelor ( vezi principiul localității  de referință ). Cicluri de combinare (cicluri de îmbinare) O altă tehnică care încearcă să reducă supraîncărcarea buclei. Dacă două cicluri învecinate se repetă de același număr de ori, atunci corpurile lor pot fi combinate până când interacționează între ele. inversarea ciclului Această metodă schimbă bucla standard while într- o buclă condițională do/while , care reduce numărul de salturi cu două atunci când bucla este executată. Împărțirea ciclului O optimizare încearcă să simplifice bucla sau să elimine dependențele din buclă împărțind-o în mai multe părți care au același corp original al buclei și intervale diferite de contor.

Optimizări ale fluxului de date

Optimizările fluxului de date se bazează pe analiza fluxului de date și de obicei iau în  considerare graficul fluxului de control și graficul fluxului de date conectate între ele ca date de intrare, precum și adesea arborele buclei și etichetarea buclei a graficului fluxului de control. Analizând, în special, propagarea informațiilor, pe aceste grafice, se relevă posibilitatea de optimizare în ceea ce privește obiectivele dorite, iar apoi se aplică optimizările.

Eliminarea subexpresiilor comune Eliminarea subexpresiilor comune este o optimizare a compilatorului care caută instanțe de expresii identice și analizează posibilitatea înlocuirii lor cu o singură variabilă care conține valoarea calculată. Convoluția constantelor Optimizări care reduc calculele redundante prin înlocuirea expresiilor și variabilelor constante cu valorile acestora. Analiza variabilelor inductive ( ing.  Analiza variabilelor de inducție ) Consultați descrierea în Optimizarea ciclului . Ștergerea înregistrărilor fără margini Ștergeți atribuirile variabilelor care nu sunt citite ulterior. Atribuirea este eliminată fie pentru că variabila nu a fost citită înainte de sfârșitul duratei de viață a variabilei, fie pentru că o atribuire ulterioară o va suprascrie.

Formularul SSA și optimizările bazate pe acesta

SSA (Single Static Assignment, Single Static Assignment) este o formă de reprezentare Data Flow Graph (DFG) în care fiecărei variabile i se atribuie o valoare o singură dată. Acest lucru evită multiplicarea arcurilor în grafic în timpul scrierilor și citirilor multiple ale unei variabile și facilitează analiza graficului. Pentru a face acest lucru, vizualizarea SSA introduce funcții Phi speciale (noduri ale graficului fluxului de date) în unele puncte de convergență din graficul fluxului de control. Aceste noi noduri sunt așa-numitele pseudo-atribuții.

Definițiile multiple sunt posibile nu numai datorită convergențelor fluxului de control ("sau"), ci și datorită posibilității de a citi o valoare compozită ca întreg, care a fost scrisă în părți prin mai multe operații ("și"). În acest caz, pentru a menține forma SSA, în interiorul blocurilor de bază sunt introduse pseudo-atribuții suplimentare, care colectează o valoare din mai multe.

Unele optimizări se bazează pe SSA. Deși unele dintre ele pot funcționa fără SSA, ele sunt cele mai eficiente atunci când SSA este prezent.

Vezi și

Note

  1. http://www.cs.uiuc.edu/class/fa07/cs498mjg/notes/optimizations.pdf  (downlink) pp. 29-30: „Înregistrați alocarea”, „Programarea instrucțiunilor”
  2. Copie arhivată . Data accesului: 25 martie 2007. Arhivat din original la 2 aprilie 2005. pagina 8, despre echivalența sarcinii de a crea un compilator de optimizare completă cu problema Halting
  3. Cooper, Keith D. și Torczon, Linda, Engineering a Compiler, Morgan Kaufmann, 2004, ISBN 1-55860-699-8 pagina 404
  4. Cooper, Keith D. și Torczon, Linda, Engineering a Compiler, Morgan Kaufmann, 2004, ISBN 1-55860-699-8 pagina 407

Literatură

  • Alfred Aho, Monica Lam, Ravi Seti, Jeffrey Ullman. Compilatoare : Principles, Techniques, and Tools = Compilatoare: Principles, Techniques, and Tools. — ediția a II-a. - M . : „Williams”, 2008. - 1184 p. - 1500 de exemplare.  - ISBN 978-5-8459-1349-4 .
  • Steven Muchnick, Proiectare și implementare avansată a compilatorului - Morgan Kaufmann, 1997 - ISBN 1-55860-320-4
  • Keith Cooper, Linda Torczon, Engineering a Compiler, Ediția a doua - Morgan Kaufmann, 2011 - ISBN 978-0-12-088478-0

Link -uri