Professional Documents
Culture Documents
• Etapa analize:
prednji deo prevodioca bavi se analizom, odnosno, velike delove koda
razbija na manje fragmente. Prednji deo je vezan za ulazni jezik, tj.
visi programski jezik.
• Etapa sinteze:
zadnji deo prevodioca bavi se sintezom, odnosno, od malih fragmenata
pravi jedinstveni izlaz. Kod zadnjeg dela je bitan izlazni jezik,
koji je najcesce asembler, ali je bitna i arhitektura, okruzenje pod
kojim se vrsi sinteza zbog optimizacije, itd.
Iako su sve faze kompilacije odvojene na ove dve velike faze, ipak
one nisu u potpunosti izolovani entiteti. Napomenimo da nije
nepoznato koriscenje prednjeg dela sa nekim delom zadnjeg dela (na
primer, analiziranje C koda se vrsi na isti nacin bez obzira na kojoj
se platformi kod kompilira, ali se koriste razliciti zadnji delovi za
Linux, Windows, i sl) ili zadnjeg dela sa nekim delom prednjeg dela
(na primer, Windows-ov .NET dozvoljava pisanje koda u C#, Visual
Basic i Visual C++, i za njih ima razlicite prednje delove, ali svi
oni se apstrahuju na zajednicki medukod koji se dalje sintetise na
isti nacin).
Takode, neko se moze zapitati u kom jeziku programiramo kompilator.
Treba znati da efikasnost kompilatora ne utice na kompilaciju. Sto je
kompilator bolji, on mora vise posla da uradi (tj. vise vremena da
potrosi) prilikom kompilacije, ali su zato programi koje dobijamo
brzi. Dakle, to ne znaci da je neophodno da kompilatore pisemo u
brzim programskim jezicima.
Faze analize:
Ono sto karakterise etapu analize jeste sto je ista i za kompilatore
i za interpretatore. Ona je veoma dobro opisana u teoriji. Deli se na
tri faze:
1. Leksicki analizator
2. Sintaksicki analizator
3. Semanticki analizator
Osnovno o fazama analize:
Zapocnimo poglavlje o etapi analize sledecim primerom: Pretpostavimo
da imamo deo programa napisanog u programskom jeziku Pascal u kojem
pise: x := 2*y1 + 3; Pogledajmo sta ce se desavati u svakoj od faza
analize pri analiziranju ovog dela koda
Leksicka analiza: Osnovno o leksickoj analizi Leksicki analizator
cita karakter po karakter iz ulazne struje karaktera i identifikuje
male, nedeljive celine koje nazivamo lekseme. Lekseme koje mozemo
izdvojiti iz primera su sledece:
x := 2 * y1 + 3 ; .
Kada se identifikuju, svakoj od leksema se pridruzuje odgovarajuca
leksicka kategorija, kao i odgovarajuca oznaka kategorije koja je
pridruzena leksemi. Ta oznaka naziva se token. Primeri tokena su
identifikator, operator, separator itd, sto se moze zapisati:
x, y1 – identifikatori
:=, *, + – operatori
2, 3 – brojevni literali
; – separator
Napomenimo da iako leksicki analizator barata sa tokenima, on ne
vodi racuna o tome da li je, na primer, neki identifikator
promenljiva, funkcija, struktura, polje itd. Time se bavi semanticki
analizator.
Osim navedenih operacija, leksicki analizator odrzava i generise
tablicu simbola u kojoj se nalazi spisak svih identifikatora na koje
se nailazilo prilikom identifikacije. Tablica simbola je struktura
podataka u kojoj se, tokom etape analize, prikupljaju informacije o
tipu, opsegu i memorijskoj lokaciji identifikatora. Ova tabela, koja
se inicijalizuje tokom leksicke analize, dopunjava se i koristi i u
ostalim fazama. Jos jedan posao leksickog analizatora jeste da pamti
brojeve linija izvornog koda.
Iako jednostavna, faza leksicke analize je veoma spora. Ovo potice
otuda sto kompilator jedino u ovoj fazi neposredno radi nad
karakterskim niskama izvornog programa, dok se ostale odvijaju nad
tokenima.
Sintaksna analiza: Sintaksna analiza proverava da li su reci (koje
dobija iz prethodne faze) sklopljene prateci gramatiku programskog
jezika. U ovoj fazi, leksicke jedinice se postupno grupisu u
gramaticke jedinice ili kategorije. Deo kompilatora koji obavlja
zadatak sintaksicke analize naziva se sintaksicki analizator ili
parser (parser ). Ukoliko bismo samo malo permutovali naredbu iz
primera, tj. ako posmatramo:
2*y1 := x + 3;
dobili bismo neispravnu naredbu jer vrednost (vrednosni izraz) 2*y1
nije l-value, tj. ne mozemo joj dodeliti neki drugi izraz. Dakle,
ulaz za sintaksicki analizator predstavljaju tokeni, i on ih
proverava, a rezultat rada sintaksickog analizatora je sintaksicko
drvo (parse tree).
Semanticka analiza: Semanticka analiza proverava tipove i deklaracije
u sintaksickom drvetu, a takode vrsi i implicitnu konverziju kod
operatora, ali i izracunavanje vrednosti (kod interpretatora).
Posmatrajmo sintaksicko drvo dobijeno semantickom analizom naredbe iz
primera. Ukoliko smo promenljivu identifikatora y1 deklarisali tipom
real, onda ce semanticki analizator implicitno konvertovati celi broj
2 u broj u pokretnom zarezu 2.0.
Faze sinteze:
114
113
112
111
110
109 -> dovde je funkcija f
107 -> ESP, ovde je bio stack pointer
106
105
104 -> 32 bitna arhitektura, dakle u 4 bajta
upisujemo 3
103
102
101
100 -> 4 bajta za adresu povratka, npr 742, to je
adresa u kod segmentu
99
98
97
96 -> U ova 4 bajta pise 114 -> to je stara vrednost
base pointer-a
95 -> ESP
Primer:
Primer:
F(3 + 2) – ovu funkciju nije moguce izracunati, zato sto nije
troadresna, morali bismo da upisemo 3 + 2 u neki registar p1, a onda
da pozovemo F(p1).
Logicki izrazi:
Definicija: Ako imamo dodelu y = x1… xn, nove zive promenljive su ove
sa desne strane. One koje nisu zive, koje su ubijene su one sa leve
strane.
Na osnovu analize zivosti mozemo da napravimo RIG (register
interference graph, graf zavisnosti medju registarima) u njegovim
cvorovima se nalaze promenjive. U grafu cemo spojiti dve promenjive
ako i samo ako su u nekom trenutku istovremeno zive. Sada vrsimo
bojenje grafa. Broj boja je broj registara. Ako nema dovoljno
registara onda neku promenjivu smestimo u memoriju. NP kompletan.
kompajleri koriste heuristike koje ne garantuju najbolje resenje.
Gledamo samo jedan osnovni blok, generisemo kod samo za taj jedan os-
novni blok, samo na nivou jednog bloka odredjujemo koja ce
promenljiva da ide u koji registar. Problem je ako imamo dva osnovna
bloka u kojima se javlja promenljiva npr. A I ta promenljiva se
javlja I u prvom I u drugom osnovnom bloku I onda primenimo alokaciju
I npr u prvom bloku joj se dodeli registar R4 a u drugom registar R6,
dakle imam istu promenljivu ali ovi prelazi nisu dobri, sta je re-
senje? Resenje je da se na kraju svakog osnovnog bloka sve
promenljive prebace u memoriju. Na pocetku svakog bloka se sve
promenljive ucitaju iz memorije. Posto smo imali a u r4 upisacemo u
memoriju, a onda cemo u drugom osnovnom bloku da ucitamo a u r5 tako
sto cemo da ucitamo nazad u r5 I tako dobijemo velik broj nepotrebnih
premestanja.
Jeste teze ali je mnogo bolje uraditi globalnu alokaciju registara
gledanjem kontrolnog toka podataka.
Kad imamo ovo, tamo gde imamo b u kodu mi upisemo r1 itd. I onda do-
bijemo isti kod ali koristimo samo 4 registra, medjutim nikada ne
moze da se desi da vise promenljivih koristi istovremeno isti regis-
tar!!! Ova analiza zivosti nam je dozvolila ovo.
Izbor instrukcija (zadatak, šabloni, prekrivanje, pregled algori tama). Za svaki troadresni kod
izabrati koju instrukciju (asemblersku) iskoristiti. Imamo registarsko i indeksno (iz memorije)
adresiranje. Da li je bolje uraditi move iz memorije, pa onda računati? Zavisi da li imamo
slobo dan registar. Ako se promenljiva često koristi u ostatku koda, bolje ju je smestiti u reg
istar, pošto je registarsko vršenje operacije brže od indeksnog (sabiranje i slično). Takođe, in
strukcije koje koriste registre imaju kraći zapis. Kako nam je dat troadresni kod, koji se može
predstaviti stablom, ovaj problem se svodi zapravo na pronalaženje prekrivača stabla, gde se
ni jedan deo prekrivača neće preklapati sa drugim delovima. Stablo se može prekriti na ra
zličite načine, pa se, isto tako, može izabrati više različitih mašinskih kodova koji odgovaraju
datoj IR. Kvalitet algoritama se može meriti na dva načina. Naime, jedna mera je koliko je
ukupno vremena potrebno da se izvrši celo stablo. Druga mera je da li postoji prekrivajne koje
kombinacijom susednih polja daje bolje rezultate. U štini, ukoliko algoritam vraća rešenje gde
je ukupno vreme potrebno za izvršavanje minimalno, onda taj algoritam ujedno vraća i
rešenje u kom se susedna polja ne mogu bolje iskombinovati. Kako RISC arhitektura ima
sužen skup instrukcija, razlika između ove dve mere za pravo i ne postoji, što kod CISC
arhitekture nije slučaj.
Raspoređivanje instrukcija (zadatak, pregled algoritama). Tražimo što manji i jednostavniji
kod nekom permutacijom i tako sman jimo prebacivanje iz memorije u registre i slično. Pi
tanje je kako to videti na osnovu grafa. Algoritam: while p o s t o j e n e p o s e c e ni u n u t r
a s n ji c v o r o vi do begin biramo c vo r n c i j i su s v i r o d i t e l j i p o s e c e ni ; n . po se
cen = 1 ; doda j_u_li s tu ( n ) ; while n a j l e v l j e d e t e m c vo ra n nema ne po s e ce ne r
o d i t e l j e && n i j e l i s t do begin m. po secen = 1 ; doda j_u_lis tu (m) ; n = m; end
end Kako koren stabla nema roditelje, zapravo nema neposećene roditelje, te je on inicijalni
čvor. Nakon što njega označimo kao posećenog, možemo dalje obilaziti njegove sinove. Obi
lazak se vrši sa leva na desno, dok god dati čvor nije list. Nakon što su čvorovi smešteni u
listu, kao rezultat ras poređivanja instrukcija se uzima obrnuta lista čvorova. Na ovaj način
smo obezbedili da u trenutku izvršavanja neke instrukcije, sva njena korišćenja su spremna
da prihvate njen rezultat.