You are on page 1of 25

1.

Kratak opis svih faza kompilacije (prikaz kroz primer)

Programski prevodilac ili jezicki procesor je program koji cita,


obraduje i prevodi kod I na neki nacin omogucava izvrsavanje
programa. Svi programski prevodioci dele se u dve grupe:

✗ Kompilatori , kod kojih je jasna razlika izmedu faze prevodenja


i faze izvrsavanja. Koriste se, na primer, u programskom jeziku C.
✗ Interpretatori , kod kojih se navedene dve faze mesaju i ne
postoji materijalizacija prevoda, tj. ne postoji izvrsni kod koji
se izvrsava vec se uvek cita ceo izvorni kod (ili medukod, zavisno
od jezika). Koriste se, na primer, u Javascript jeziku.

Sve faze kompilacije mogu se podeliti u dve etape:

• 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:

Nekada se citava ova etapa naziva Generisanje koda. Ova etapa se


temelji na specicnim, cesto ad hoc, resenjima koja zavise od
konkretne masine. Deli se na tri faze:
1. Generisanje medukoda{ primeri medukodova su: P-kod (P-code)
programskog jezika Pascal i Bajt kod (Bytecode) programskog jezika
Java
2. Optimizacija medukoda sprovode se komplikovane transformacije
3. Generisanje koda generise se izvrsni kod

2. Stabla apstraktne sintakse (reprezentacija u C-u i C++-


u, kreiranje tokom parsiranja, atributi cvorova)
Zavisi od toga koji se jezik koristi. Ako koristimo C, cvorovi stabla
su strukture. Grane ka drugim cvorovima su pokazivaci. Moramo da
cuvamo informaciju kog tipa je taj cvor, da li je konstanta,
promenljiva (informaciju da je u pitanju promenljiva i ime
promenljive), operatori itd. U OOP se vise ne koriste strukture za
predstavljanje cvorova se koriste objekti/hijerarhija klasa. Clang
ima klase za reprezentaciju AST-a i tu je dovoljno nauciti njegove
gotove klase. Izlaz sintaksne analize je apstraktno sintaksno stablo,
pravi se npr. pri koriscenju Yacc-a. Atributi cvorova u C-u
predstavljaju elemente strukture, a u OOP predstavljaju atribute
klasa.
Primer u C-u: typedef enum { typeCon, typeId, typeOpr } nodeEnum;
Imamo cvor konstante – strukturu koja u sebi ima jedino vrednost,
cvor identifikatora – struktura koja sadrzi npr. string koji
predstavlja ime identifikatora, struktura operatora – moze da ima
broj operanada, tip operatora itd. Atributi su dakle stvari koje se
nalaze unutar struktura.
Primer C++:
class Konstanta {
public:
Konstanta(int x) : value(x) {}
private:
int _value;
};
Atribut klase konstante je vrednost. Kompletno apstraktno sintaksno
stablo se dobija koriscenjem hijerarhije klasa. Npr. ukoliko zelimo
da predstavimo izraze, na vrhu hijerarhije bi se nalazila apstraktna
klasa koja bi imala cisto virtualan metod za izracunavanje izraza, a
ostale klase naslednice: konstanta, promenljiva, operatori bi imale
implementiran taj metod.

3.Semantička analiza (zadaci, statički i dinamički tipovi,


reprezentacija tipova i provera tipova)
Primer :
int *x;
int a[5];
int b2;
a[a[*x]+10]+b2+*x;
Osnovni zadatak semanticke analize je provera tipova. Ako izraz
predstavimo sintaksnim stablom, znacemo tipove listova, a onda cemo
polako zakljucivati tipove kako idemo uz sintaksno stablo.
Kompilator treba da proveri sto vise moze. Ali neke stvari prosto
nije moguce proveriti. Ako imamo npr. i = 15, i onda x[i] ->
kompilator moze nekom dubljom analizom da proveri da li pristupamo
elementu u okviru granica, medjutim ukoliko ucitamo i sa standardnog
ulaza I onda pokusamo da pristupimo elementu niza sa x[i] nije moguce
odrediti da li se pristupa u okviru granica u fazi kompilacije.
Kompilatori cak i onaj prvi slucaj obicno ne rade.
U teoriji postoje staticka i dinamicka provera tipova. Ukoliko
kompilator proverava tipove onda se radi o statickoj proveri, ukoliko
se tipovi proveravaju u toku izvrsavanja programa onda se radi o
dinamickoj proveri tipova.
Provera korektnosti tipova implicitno ukljucuje I proveru da li je
svaka promeljiva deklarisana, ukoliko proveravamo tipove I naidjemo
na promenljivu koja nije deklarisana, semanticki analizator ne moze
da proveri kog je tipa promenljiva koja uopste nije deklarisana,
takodje se proverava da li je neka promenljiva visestruko
deklarisana.
Provera tipova jeste najznacajniji zadatak semanticke analize, ali
postoje stvari koje treba da proveri a nisu vezane za tipove.Na
primer ne sme da stoji continue van neke petlje... Kako resavamo ove
probleme? Prvi pristup je da modifikujemo gramatiku (imamo naredbe
koja sme da stoji unutar petlji i van petlji) medjutim tim bi se
gramatika znacajno uslozila. Pravi pristup je da se continue van
petlje smatra sintaksno ispravnim a onda u semantickoj analizi kad
proveravamo tipove proveravamo i da li se naredbe javljaju na
odgovarajuci nacin, kad obilazimo sintaksno stablo mozemo da
napravimo poseban obilazak sintaksnog stabla koji ce da proveri za
svaki continue/break da li se on nalazi ispod nekog cvora petlje ili
ne. To je mnogo jednostavnije. Vrlo cesto na nivou stabla nema
potrebe razlikovati razlicite vrste petlji. Mogli bismo da uvodimo
razlicite cvorove. Obicno se to ne radi, vec npr. for(i = 0; i < 10;
i++) ovo se poveze kao 3 naredbe, prva stvar je dodela, drugi je
jedan while cvor i u telu ima i++ npr. Sta smo postigli izbegavanjem
cvora for? Pa sve optimizacije petlji ce optimizovati na isti nacin,
nije bitno da li je u programu koriscena for ili while petlja to ce u
sintaksnom stablu biti isto prikazano. Kompilator nam for shvati kao
samo jedno sintaksno obogacivanje while petlje.
Tipovi: U teoriji se ovo naziva konstruktor tipova, imamo osnovne
tipove, jedan jedini osnovni tip u primeru iznad je integer, mozemo
pps da ih ima vise, int, char, float itd. a onda imamo konstruktore
koji nam od jednostavnijih grade slozenije tipove. Npr. u ovom
primeru imamo pokazivacke tipove. Mozemo da usloznjavamo. Drugi
konstruktor je a[5], koji dalje moze da se usloznjava. Imamo slozenu
strukturu tipova, tipovi dobijaju strukturu. Posto tipovi imaju
strukturu njih ima smisla predstaviti izrazima (mozemo da gradimo
strukturu pointer na pointer na pointer od niza na pointer itd.)
dakle ovo se predstavlja stablima! Radi ustede memorije u kreiranju
drveta postoji mogucnost koriscenja aciklicnog grafa, da ako imamo
vise cvorova integera da je to u stvari jedan cvor i svi ostali da
pokazuju na njega.To je moglo raditi i u prethodnim koracima
pravljenja sintaksnog stabla.
Cvor u stablu tipova:
enum types {T_ERROR, T_CHAR, T_INTEGER, T_POINTER, T_ARRAY};
typedef struct _type {
enum types tag;
union {
struct _type* ptype;
struct {
int num;
struct _type* ptype;
} atype;
};
} type;
4.Tabele simbola (uloga i nacini implementacije)

Zadatak tablice simbola je da preslikava imena promenljivih u nesto


(void*). Postavlja se pitanje kako implementirati tablicu simbola?
Pozeljno je da se podrzava efikasna pretraga. U PPJ smo to
implementirali binarnim drvetom. Slozenost najgoreg slucaja za
ubacivanje u takvo binarno drvo je linearna, najbolja je
logaritamska. Dakle ako hocemo efikasnost moramo da se potrudimo oko
implementacije. Pored drveta se cesto koristi implementacija u obliku
hes tablice.
Jednostavan nacin implementacije hes mape je niz, svaki element niza
predstavlja povezanu listu. Kada ubacamo novi identifikator na osnovu
imena identifikatora izracunamo indeks niza u koji taj identifikator
treba ubaciti, ukoliko lista nije prazna tj. doslo je do kolizije,
element ce biti dodat na pocetak liste (jer je brze). Veoma je bitno
da se elementi ravnomerno rasporedjuju po nizu, za ovaj posao se
koristi hes funkcija (za hes funkciju se obicno koriste neki cudni
brojevi, magicne konstante za koje je utvrdjeno da imaju takva
svojstva).
Veza dosega I tablice simbola:
Kako se organizuju tablice simbola da bi mogli da imamo koncept
dosega (scope)? Kad imamo deklaraciju x, dokle to x doseze (od prvog
begin-a do odgovarajuceg end-a u Pascal-u)? Tesno pitanje je o
konfliktu identifikatora, npr imamo dole niz x od 3 karaktera a iznad
x koje je integer, dole treba da bude aktuelno da je x niz od 3
karaktera, medjutim kad izadjemo iz bloka onda je x ponovo integer.
Ono sto je kljucno, je da moramo da imamo podrsku za ovo na nivou
tablice simbola.
Dakle tablica simbola je organizovana u hes tablicu. Sta raditi sa
dosegom? Ono sto je kljucna stvar je da trebamo modifikovati tu
tablicu simbola, kad naidjemo na y prvi put, onda u tablicu simbola
treba ubaciti informaciju da je nova promenljiva, a onda kad naidjemo
na end, u tom trenutku trebamo u tablici simbola da izbrisem y. Za
ovo imamo dva pristupa:
1. Stek tablica simbola! Imamo tablicu simbola, kad otvorimo novi
doseg onda napravimo novu tablicu simbola i u njoj uvodimo
promenljive koji su za taj doseg relevantni. Dakle pravimo stek
tablica simbola, tablice simbola jedne ispod drugih. Kako ide
mehanizam pretrage tablice simbola? Nas zanima tip promenljive y,
trazimo odozdo! Ako ga nema na vrhu steka (koji je najnize) onda
idemo u tablicu iznad, tablicu iznad, ako dodjemo do dna steka (vrha
memorije) i ne nadjemo ga onda kazemo da promenljiva nije
deklarisana. Ako se pojavi novi doseg u kome mi kazemo da je na
primer x tipa array. Kad pitamo kog je tipa promenljiva x on treba
da nam kaze da se radi o array-u bez obzira sto iznad mozda ima neki
drugi tip za x. Svaka nova tablica "sakriva" one prethodne.
2. Flat tablice - Pristup iznad je malo teze implementirati u C-u
pa koristimo mehanizam FLAT tablica, nemamo hijerarhijski organiovane
tablice vec sve pisemo u jednoj tablici. I dalje se drzimo hesiranja
ali dakle necemo vise imati vise hes tablica nego jednu. Prvo su na
pocetku svi elementi u tablici prazni (null pokazivaci). Na pocetku
svakog novog dosega na stek postavljamo null, na kraju scope-a
brisemo sve promenljive sa steka do tog scope-a I iz tablice simbola.
Dakle ne moramo da koristimo visestruke tablice, mozemo da imamo
jednu tablicu a da sa strane u steku jedino cuvamo redosled
deklaracije sa nekim markerima pojedinacnih dosega.

5. Asemblerski i mašinski jezici (registri, organizacija


memorije, instrukcije, odnos RISC\CISC)
Prvi programski jezici pisani su na masinskom jeziku. Masinski jezik
je skup instrukcija procesora. Sve masinske instrukcije I njihovi
operandi predstavljaju nizove binarnih cifara. Program napisan u
masinskom jeziku predstavlja niz binarnih cifara. Adrese koje su se
javljale u programima bile su apsolutne I kao takve program je mogao
da radi korektno samo ako je pocinjao od apsolutne adrese navedene u
programu. Programer je morao da zna binarne ekvivalente masinskih
instrukcija kao I da korektno prevede dekadne vrednosti brojeva u
binarni zapis. Takav program je bio ucitavan u racunar I uz pomoc
posebnog programa upisivan na one adrese u memoriji koje su navedene
u programu. Izvesno olaksanje za programera predstavljala je
mogucnost kodiranja programa u heksadekadnom umesto u binarnom
obliku.
Asemblerski jezik je uveo simbolicke oznake za operacije I brojeve,
ove simbolicke oznake su omogucile povecanje produktivnosti
programera I vecu citljivost programa. Programi su I dalje pisani u
apsolutnim adresama a poseban prevodilac prevodio je simbolicke
oznake I njihove operande u binarni ekvivalent.
Registri predstavljaju najbrzu memoriju racunara, skupi su pa ih ima
malo. Registri su ugradjeni u procesor I u njima se nalaze podaci
koje procesor trenutno obradjuje. Na primer, ako je potrebno izvrsiti
aritmeticku operaciju nad dva broja, ti brojevi se najpre smestaju u
registre. Registri su memorija koja privremeno cuva podatke (podaci
se gube nakon iskljucivanja racunara). Registara ima oko 30tak na
CISC procesorima, a na RISC procesorima ih moze imati do nekoliko
stotina.
Memoriju je moguce podeliti na 4 segmenta:
- Stek segment: sadrzi lokalne promenljive I parametre funkcija.
- Hip segment: je deo memorije u koji se smestaju podaci koji se
generisu u toku izvrsavanja procesa, odnosno prostor koji se
dinamicki alocira u zavisnosti od potreba procesa.
- Segment podataka: sadrzi globalne promenljive
- Kod segment: Sadrzi instrukcije koje proces treba da izvrsi
CISC – Complex instruction set computer. Karakterise ga veliki broj
instrukcija, ukljucujuci I slozenije, veliki broj registara
specijalne namene a manji broj registara opste namene. RISC – Reduced
instruction set computer, podrzava samo minimalni broj instrukcija
dok se funkcionalnost slozenijih instrukcija postize kombinacijom
jednostavnijih, ima veliki broj registara opste namene.

6.Generisanje koda za stek masinu i stek masinu sa


akumulatorom

Stek masina je racunar koji koristi LIFO stack za sve privremene


vrednosti. Vecina instrukcija pretpostavlja da se operandi nalaze na
steku I rezultati se takodje smestaju na stek. Npr ukoliko zelimo da
saberemo dve vrednosti koristimo add instrukciju, add instrukcija
skida sa steka poslednje dve vrednosti a rezultat se stavlja na stek.
Postoje I slozenije instrukcije koje mogu da rade sa vise parametara.
Neke stek masine imaju ogranicenu velicinu.
Stek masina sa akumulatorom moze cuvati privremene vrednosti u akumulator
registru. Akumulator je jedan registar opste namene.

7. Aktivacioni slogovi (stek okviri i njihov sadrzaj,


konvencije pozivanja funkcija)
Pozivi funkcija se ostvaruju u stekolikom redosledu. Pozovemo
funkciju main posle main pozovemo fju f, ispod main-a se onda u
memoriji stave svi podaci karakteristicni za f, onda f pozove g, onda
poziv g dodje iznad f, g se zavrsi i skine se, f se zavrsi i skine
se, pa se vratimo u main itd. U svakom trenutku na stek stavljamo
podatke vezane za svaku specificnu funkciju. Pozivi funkcija bice
predstavljeni memorijskim oblastima koje se nazivaju stek okviri koji
se slazu jedan ispod drugog.

Stvar koja je karakteristicna za funkcije su parametrei. Kad pozovemo


funkciju mi njoj prosledimo parametre. Kako ih prosledjujemo? Upisemo
ih u stek okvir. Tu ima nejasnoca, da li upisati u svoj stek okvir
ili u stek okvir funkcije pozivaoca. To se zove calling convention,
postoje razliciti mehanizmi kako se vrsi poziv funkcije. Neke
konvencije rade na jedan nacin, neke na drugi, ali se svakako u stek
okviru nalaze prosledjeni argumenti. U stek okviru bi trebao da se
nadje prostor rezervisan za lokalne promenljive. Lokalne promenljive
se alociraju u njenom stek okviru. Dok funkcija f traje tu imamo sve
podatke, kad nam vise ne treba skidamo ceo stek okvir i u tom
trenutku sve njene promenljive bivaju uklonjene. Mi u C-u i parametre
funkcije mozemo da tretiramo kao lokalne promenljive koje
koristimo/menjamo.

Na 64bitnoj arhitekturi je zaista scenario da se koristi 6 registara


preko kojih se salju prvih 6 parametara a sve ostalo se salje preko
memorije. Zasto je bolje upisati parametre u registre? Brze je.
Pristup memoriji kosta. Zasto je nezgodno da se sve salje preko
registara? Pa malo ih je, nemamo mesta. Ako 6 registara upotrebimo
samo za ulazne parametre onda manje mozemo da koristimo za
izracunavanje. Ako zelimo optimalan kod jako je bitno optimizovati da
sto vise upisujemo u registre a sto manje upisujemo u memoriju.
Veliki podaci se moraju pisati u memoriju. Tu se javlja pitanje i
samog jezika. U C-u nije moguce proslediti niz u funkciju. Jedino sto
mozemo da posaljemo je adresa pocetka niza koja staje u 64bita
(registre). Problem moze da bude sa strukturom koje mozemo da saljemo
kao argumente, ako je parametar strukturnog tipa on se ne prenosi
preko regiistara prosto jer se ne zna kolika ce ta struktura da bude,
dakle upisuje se u memoriju.

U nekim slucajevima je zgodno imati adresu pocetka prethodnog stek


okvira.To se zove backlink ili static link. Zasto? Zato sto u nekim
slucajevima funkcija koja je pozvana moze da pristupi promenljivama
funkcije koja ju je pozvala. U C-u toga nemamo. U Javi imamo
ugnjezdene klase gde se desava bas ovo.

Ono sto je takodje nekad sadrzaj stek okvira je sadrzaj odredjenih


registara koje funkcija koja je pozvana treba da sacuva. Postoji
konvencija: Kad funkcija f pozove funkciju g u tom trenutku se u
registrima nalaze neki podaci, kad se zavrsi funkcija g ono sto je
pisalo u registrima pre njenog poziva mora ponovo biti zapisano u
registrima. Sta se onda desava sa funkcijom g? Ona mora da garantuje
da ce da ostaviti registre nedirnute. Sta onda da radi funkcija koja
je pozvana? Jedan scenario je da ne diramo registre ali to je tesko
jer su potrebni, drugi scenario je da na pocetku u svoj stek okvir
fja zapamti informacije druge fje, onda radi svoje a na kraju kad
zavrsi vraca sa steka sve registre tamo gde je trebalo. Jedan od
najznacajnijih registara koji se tako cuva je base pointer.

JEDNA KONKRETNA KONVENCIJA POZIVANJA: THE C CALLING CONVENTION 32bit


PROGRAMS

U 64-bitnim programa konvencija pozivanja je znacajno drugacija jer


se koristi vise registara. Imamo caller i calee. Caller je funkcija
koja poziva, a calee je ona koja je pozvana. Caller je funkcija f, a
callee je funkcija g. Imamo stek pokazivac na vrh steka. Sad je
pitanje kako da znamo dokle je taj stek popunjen, gde treba da pocne
novi stek okvir? Postoji poseban registar koji se na 32bitnoj
arhitekturi naziva ESP - SP je od stackpointer, RSP je na 64bitnoj
arhitekturi. To je registar u kome pise adresa vrha steka, odnosno
ispod popunjenog prostora! Sve ispod te adrese je prazan stek. U
realnim implementacijama iz istorijskih razloga obicno je stack
downwert going - to znaci da raste od visih ka nizim adresama. Tako
da brojevi treba da opadaju.

Caller - onaj ko je pozivao funkciju, npr f: g(3),ce na stek da stavi


argument poziva, u ovom slucaju caller treba da upise 3 na stek.
Dakle prvi parametar je prvi pa tek onda idu ostali. Nakon toga se
izvrsava CALL instrukcija koja prebacuje kontrolu pozvanoj funkciji.
Sta radi CALL instrukcija? Prva stvar je da upamti adresu povratka,
prva adresa instrukcije koju treba da izvrsimo nakon call je adresa
povratka. Kako znam koja je adresa instrukcije koja se trenutno
izvrsava? Postoji registar instruction pointer ili program counter u
kome pise adresa sledece instrukcije. Ako instrukcija poziva funkcije
ima adresu 738 ja cu na stek da stavim 742 za sledecu instrukciju. U
instruction pointer se upisuje adresa prve instrukcije iz kod
segmenta funkcije g, Dakle CALL automatski uradi push na stek adrese
povratka i azurira instruction pointer.
CALEE - Dolazimo do price o snimanju registara. CALEE mora neke
registre da odrzi u zivotu, jedan od jako vaznih registara je BASE
POINTER. Stack pointer ukazuje na kraj stek okvira neke funkcije. Ali
nekad je bitno da znamo gde je pocetak stek okvira neke funkcije. U
registru base pointer se cuva adresa pocetka stek okvira neke
funkcije. Ako sam trenutno bio u funkciji f u EBP trenutno postoji
adresa pocetka stek okvira funkcije f, kad krenem da izvrsavam G
zelim da tu pise adresa pocetka stek okvira funkcije g, medjutim kada
bih ja izmenio ebp ja bih ga pokvario, kada se kasnije vratim u f
izgubio bih informaciju o njenom pocetku stek okvira. Sta je resenje?
Snimimo ga na stek! Ja sada u registar EBP upisujem trenutnu vrednost
ESP-a, time sam uradio da kad sam sve slozio tek u tom trenutku
pocinje stek okvir funkcije g. U stek okviru funkcije f se onda
nalazi: parametri poziva funkcije g, adresa povratka, stara vrednost
base pointera i tek onda nakon toga pocinje stack okvir funkcije g.
Tek onda alociram prostor za lokalne promenljive, privremene
rezultate itd., kompajler zna da vidi koliko ce mu prostora biti
potrebno i onda povuce ESP dole za potreban broj mesta. Ako bi g
pozvao nekog dalje, on bi slozio argumente za h, adresu povratka i
staru vrednost base pointer-a i tek nakon toga bi nastao stack okvir
fje h. STA SE DESAVA PO IZLASKU IZ FUNKCIJE?Funkcija g umanji
vrednost ESP i skine sve svoje lokalne promenljive, nakon toga uradi
pop EBP da
vrati base pointera na pocetak stek okvira funkcije f. Nakon toga
uraid instrukciju RET koja skine sa steka vrednost adrese i stavi je
u PROGRAM COUNTER tj. INSTRUCTION POINTER. I onda nastavlja
izvrsavanje funkcije f. STA f treba da uradi tokom nastavka
izvrsavanja?Da ukloni parametre poziva funkcije g koje je postavio.
U standardnoj C konvenciji pozivanja dogovor je da se nalazi u
registru EAX. Za strukture vaze posebna pravila. Za standardne
tipove podataka se vraca za EAX.
Ovako tece poziv jedne funkcije...

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

8.Medjujezici i medjukod (troadresni kod, nacini


reprezentacije, generisanje troadresnog medjukoda, logicki
izrazi)

AST je prvi oblik medjureprezentacije. Na osnovu ulaza smo uradili


leksicku, sintaksnu analizu, AST potom prodje kroz semanticku
analizu. Za AST mozemo generisati kod rekurzivno (za neki asembler).
Medjutim medjureprezentacija u vidu stabla je mnogo bliza ulazu nego
izlazu. Pozeljno je izgraditi drugu medjureprezentaciju koja ce biti
malo bliza izlazu.

Pokazalo se da je zgodnije napraviti neku medjureprezentaciju nego


odmah napraviti asemblerski kod. Medjureprezentacija treba da bude
detaljnija od ulaznih podataka ali manje detaljnija od izlaza. Dakle
mi trebamo konstruisati neki asembler viseg tipa. U pravom asembleru
imamo unapred zadat skup registara koje mozemo da koristimo, kada
zelimo da pozovemo funkciju moramo voditi racuna o vise stvari, kako
tacno izgleda stek okvir, ko slaze argumente, gde se stavlja adresa
povratka, gde se nalaze lokalne promenljive itd. Svaki asembler imao
je drugacije implementirane instrukcije. Dakle ukoliko bismo odmah
generisali asemblerski kod, taj kod bi bio neupotrebljiv na drugoj
arhitekturi. Generisacemo medjukod koji se kasnije moze prevesti na
asemblerski kod za bilo koju arhitekturu racunara. Npr. mozemo da
napravimo prevodjenje iz C-a u taj medjukod, iz Paskala u taj
medjukod, svi se prevode u zajednicki asemblerski medjukod, a iz tog
medjukoda mozemo da generisemo asemblerski kod za sve arhitekture.
Oko 90% optimizacija se vrsi na nivou medjukoda (10% optimizacija su
prilagodjene arhitekturi), iz ovog razloga je napravljen llvm jezik
(low level virtual machine) koji je apstraktan asembler, llvm kod se
moze konvertovati na odredjenu arhitekturu (mi pravimo frontend,
leksicku, sintaksnu I semanticku analizu). Kada je god potreban
registar llvm-u on “napravi” novi, %1, %2 itd. Ovi registri ce se
preslikati u konkretne registre prilikom generisanja finalnog koda.

Najcesca medjureprezentacija je troadresni kod. LLVM je specijalna


vrsta troadresnog koda. Kako izgleda troadresni kod?

Primer:

x+y*z – pomnozi y I z I stavi taj rezultat u registar p1, vrednost


rog registra sabere sa x I smesti u registar p2. Troadresni kod ne
znaci da imamo tacno tri adrese, vec da imamo najvise tri adrese.

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).

Neki kompilatori umesto sintaksnog stabla koriste reprezentaciju u


obliku aciklicnog grafa (DAG – directed acyclic graph). Kada
primetimo da se neki izraz ponavlja, umesto dve kopije u DAG-u bismo
imali jednu I obe grane bi pokazivale na taj cvor. Savremeni
kompilatori ne rade DAG zato sto optimizator kasnije optimizuje ove
stvari. Eliminacija zajednickih izraza jedna je od cestih
optimizacija.

Troadresni asemblerski jezici dopustaju pristup steku. Dozvoljeni su


uslovni I bezuslovni skokovi, definisanje labela.

Implementacija generisanja koda za troadresni asembler nije


komplikovana. Dovoljno je imati jednu funkciju sa switch-om, npr za
zbir generisemo kod za levu stranu I smestamo u neki registar e1, za
desnu stranu generisemo kod I smestamo u neki registar e2. Zbir ova
dva registra se upisuje u treci registar I predstavlja rezultat.

Ovako generisan kod moramo cuvati u memoriji da bi ga optimizovali.


Troadresni kod se interno reprezentuje uz pomoc niza cetvorki. Prvi
element u nizu je operacija, drugi (opcionalan) I treci su operandi I
cetvrti element je mesto gde se smesta rezultat. Postoji jos jedna
reprezentacija u vidu trojki, kada postoji podrazumevani registar za
rezultat, cetvorke su zgodnije za optimizaciju.

Dakle mozemo da imamo vise frontendova koji se prevode na isti


medjukod a onda od medjukoda mozemo da napravimo zadnji deo
kompilatora za razlicite arhitekture.
Tri kljucne faze u delu generisanja koda nakon medjukoda: odabir
instrukcija (npr. ako imamo a=a+1 , bolje je da se prevede kao a++
zato sto je brza operacija uvecanja nego sabiranja), prica o
odredjivanju redosleda instrukcija (nekad je moguce permutovati
redosled instrukcija, dobijamo isti rezultat ali brze) i treca fazna
faza je mapiranje beskonacnog skupa registara na konacan skup. Obicno
su druga i treca faza isprepletane.

Logicki izrazi:

Prevodjenje logicnih izraza zavisi od programskog jezika. Mozemo


imati logicki izraz x >= 5 koji ima vrednost 1 ili 0 I nakon toga na
tu vrednost dodati neki broj, npr. 55, dakle logicke izraze mozemo
posmatrati kao obicnu vrednost. Medjutim logicki izrazi mogu biti I
deo kontrolnih struktura pa je potrebno generisati medjukod koji ima
jump-ove organizovane na osnovu vrednosti izraza. Ja mogu na osnovu
izraza da generiisem odgovarajuce uslovne i bezuslovne skokove. Ovo
je malo optimalnije nego da smo imali izracunavanje izraza pa
smestanja u registar pa tek onda skokove na osnovu vrednosti tog
registra.

9.Osnovni blokovi i graf kontrole toka (odredjivanje,


primeri)

Osnovni blokovi se formulisu na troadresnom kodu. Kod treba podeliti


na blokove tako da za svaki blok znamo da se u tom bloku izvrsavaju
sve instrukcije od prve do poslednje i da ne mozemo da iskocimo iz
tog bloka kao i da ne mozemo da uskocimo u taj blok.
Primer:
t=2*x pa onda w=t+x
Kako ovo moze da se optimizuje? Umesto da izracunamo 2*x pa da dodamo
x, lakse je da izracunamo odmah 3*x, Sta bi se desilo da ovo nije
osnovni blok? Onda ne bi mogli da zamenimo sa 3*x jer bi se mozda x
promenilo negde izmedju t=2*x i w=t+x.
Kako se odredjuju osnovni blokovi na osnovu koda?
Treba da odredimo LEADERS - instrukcije kojima pocinju osnovni
blokovi. Pocetna instrukcija je vodja bloka. Svaka instrukcija na
koju moze da se skoci je sigurno vodja, sigurno je vodja instrukcija
nakon grananja. Nakon toga definisemo blokove kao kodove od vodje do
vodje.
Graf kontrole toka
Sledeca stvar koju treba da odredimo jeste graf kontrole toka.
Cvorovi grafa su osnovni blokovi. Ono sto je bitno za kontrol flow
graf je kojim redosledom se izvrsavaju blokovi. Kako odrediti
strelice? /svaki osnovni blok ima strelicu ka bloku koji odgovara
instrukciji ispod njega osim u slucaju kada je poslednja instrukcija
GOTO. Ako se radi o bezuslovnom skoku onda vodimo na odgovarajucu
labelu a ako se radi o uslovnom skoku imamo dve grane gde jedna grana
vodi ka odgovarajucoj labeli a druga ka sledecem bloku. Skok moze
biti iskljucivo poslednja instrukcija u bloku.
10.Analiza toka podataka (primeri, jednacine - in, out,
kill, gen, vazece definicije, use-def/def-use lanci)

Analiza toka podataka (Data flow analysis), potrebno je da imamo


nekakvu vrstu analize toka podataka kako bismo mogli da zakljucimo da
li je neka optimizacija moguca ili ne. Takodje pri generisanju koda,
za registarsku alokaciju je potrebno izvrsiti analizu toka podataka.
Ako je neka promenljiva mrtva u nekoj tacki, registar u kome je ona
smestena se sme dodeliti nekoj drugoj promenljivoj. Ako imamo dve
promenljive koje su takve da nisu zive istovremeno, mogu da Ih
smestim u isti registar.
Analiza toka podataka se deli na analizu nanize (analiza dosega
definicije) I analizu navise (analizu zivota promenljive).
Za svaku naredbu uvodimo odgovarajuce kontrolne tacke pre naredbe I
posle naredbe, tako da ako osnovni blok ima n naredbi, blok ce imati
n + 1 kontrolnu tacku.
Vrednost neke promenljive odredjuje se na osnovu ulaznih grana.
Ukoliko imamo n ulaznih grana, vrednost promenljive racunamo kao x=
sup(x1,x2...xn), gde Xi predstavlja vrednost promenljive na izlazu I-
tog bloka. Vazi: sup(c, neTe) = c, sup(c,c) = c, sup(T, bilo sta) =
T, sup(c, d) = T, sup(neTe, neTe) = neTe.
Dakle:
Na osnovu leksicke I sintaksne analize pravimo AST. AST mozemo se-
manticki analizirati (uz pomoc tablice simbola gde su zapisane sve
deklaracije promenljivih, tipovi itd.) prodjemo kroz AST I vidimo da
li se sve pojavljuje kako treba. Usput mozemo imati implicitne kon-
verzije gde su potrebne. AST koje je semanticki ispravno -> prebacu-
jemo u medjukod (najcesce troadresni kod). Kad imamo troadresni kod,
onda delimo taj kod na osnovne blokove (parcici koji se linearno
izvrsavaju u koje ne mozemo da uskocimo I da iskocimo). Osnovne
blokove njih mozemo da organizujemo u graf kontrole toka programa
(jasno se na grafu mogu prepoznati grananja). Graf kontrole toka je
struktura nad kojom vrsimo razlicite korisne operacije. Razlikujemo
susitinski dve grupe optimizacija, lokalne I globalne optimizacije.
Lokalne su optimizacije pojedinacnih osnovnih blokova. Globalne –
gledamo ceo graf kontrole toka I optimizujemo ga. Neke optimizacije
se lako mogu zakljuciti ukoliko se troadresni kod prevede u SSA kod
(jednu promenljivu koristimo samo jednom). Da bismo vrsili razlicite
globalne analize potrebno je da vrsimo analizu toka podataka.
11.Zivost promenljivih (definicija, algoritam odredjivanja
zivih promenljivih)

Primer nekog algoritma +


Postoji live in I live out. Za svaku instrukciju se definisu dva
skupa line in I line out. Live in – su zive promenljive prilikom
ulaska u naredbu. Live out za naredbu? Sta je zivo posle te naredbe!
Live in moze da se definise I za blok I live out za blok moze da se
definise. Dakle bilo na nivou bloka bilo na nivou naredbe mozemo da
definisemo live in I live out. Svaka naredba ubija neke zive
promenljive I generise neke nove zive promenljive.

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.

12.Oblik staticke jedinstvene dodele (SSA)

SSA static single asingnment (jednostruka dodela):


x= z+y x1=z1+y1
a=x a1=x1
x=2*x x2=2*x1
t=x+3 t1=x2+3
Svaki registar se nalazi samo jednom sa leve strane i registri se ne
koriste pre nego sto im se vrednost dodeli. Kod se prevodi u SSA zbog
optimizacije – nista ne koristimo dva puta i to nam otvara vrata za
nov optimizacije.
Kada prevodimo ceo graf kontrole toka u SSA to postaje
problematicnije. Na primer ukoliko imamo grananje i u oba cvora imamo
x sa leve strane ne znamo koje x bi bilo x1 a koje x2. Zbog toga se
korist PHONY ( lazna instrukcija) i ona zna iz koje smo grane dosli i
koje x koristimo. x3= PHI( x1, x2) nadalje koristimo x3. Kada imamo
SSA eliminacija zajednickih podizraza je trivijalna. Ako vidimo da
postoje dve iste desne strane garantujemo da su one jednake. Ovo
povlaci dalje optimizacije. Nijedna lokalna optimizacija ne radi puno
sama za sebe ali povlaci drugu optimizaciju. Mozemo da radimo jednu
po jednu optimizaciju, a ukoliko je vreme ograniceno u nekom trenutku
cemo stati sa optimizacijama a sigurni smo da je ekvivalentan i brzi
od pocetnog.

13.Lokalna optimizacija koda (definicija, najcesci oblici


optimizacije)

Najcesce se optimizuje vreme izvrsavanja, nekad je potrebno


optimizovati velicinu asemblerskog koda(uklanjamo mrtav kod). Ako
imamo mali program instrukcija ce se naci u kesu a ako imamo veci
program instrukcija se nece naci u kesu tako da je manji program
bolji. Optimizacije mogu da bude i u drugim segmentima: komunikacija
preko mreze, utrosak struje. Optimizacija u velikom broju slucaja ne
postize optimalan kod ali tezi ka tome. Kompilator koji generise brzi
izvrsni kod uglavnom sporo kompajluje zato sto treba da uradi vise
stvari. Na velikim projektima brzina kompilacije moze biti bitna.
Bitna osobina optimizacije je ekkvivalentnost, da program daje iste
rezultate pre i nakon optimizacije.
Postoje lokalne optimizacije (optimizacija jednog osnovnog bloka, to
su najjednostavnije optimizacije), globalne ( koriste ceo graf
kontrole toka) i interproceduralne.

Lokalne optimizacije: algebarske, eliminacija mrtvog koda,


eliminacija zajednickog podizraza, zameana redosleda
Algebarske transformacije: neke instrukcije se mogu obrisati, npr.
x=x+0 ili x=x*1. Uproscavanje: x=x*0 kao x=0, mnozenje stepenom
dvojke se prebacuje u shiftovanje koje je brze [ x=x*15 > x=x*16-x
(*16 je siftovanje) ovo je zamenna instrukcija efikasnijima]. Jedna
transformacija sama po sebi ne menja puno ali jedna transformacija
povlaci drugu, druga povlaci trecu itd.
Constant folding: izracunavanje konstantnih izraza x=2+2 kao x=4.
Ukoliko imamo uslovne skokove i zakljucimo da je uslov uvek tacan
tada uslovni skok mozemo zameniti bezuslovnim, a ukoliko je uslov
uvek netacan onda tu instrukciju mozemo da obrisemo. Na ovaj nacin
mozemo spojiti dva bloka.
Eliminacija mrtvog koda: to su nedostizni blokovi, uklanjanje koda
cini program manjim, manji programi su brzi.Na primer ukoliko imamo
x=5*y i onda do kraja programa nigde ne koristimo x, dakle x se nigde
nije javio sa desne strane samim tim je kod x=5*y mrtav kod. Mrtav
kod cesto nastaje uslod optimizacija.
Eliminacija zajednickih podizraza: Na primer ukoliko imamo x=z+y …
(neki kod)… w=z+y – mozemo napraviti transformaciju w=x ako se u kodu
izmedju x nije menjalo.
Primer:
b=z+y , a=b , x=2*a
Mozemo da kazemo x=2*b , a se vise nigde ne koristi pa ga mozemo
ukloniti, a = b postaje mrtav kod.
Primer:
a=5 , x=2*a , y= x+6 , t=x*y
x=2*5 => y=10+6 => t= 10 *16 => t = 10 << 4
Sve osim poslednje instrukcije je mrtav kod.
Peephole optimization: vrsi se u trenutku generisanja asemblerskog
koda, posmatra 3-4 susedne instrukcije i trazi sablone koje moze da
optimizuje.
Primer:
move ra rb
move ra rb
Ovo se optimizuje u: move ra rb

14.Globalna optimizacija koda (definicija, primer -


globalna propagacija konstanti)
Kod globalne optimizacije optimizujemo istovremeno vise blokova.
Posmatramo vise blokova jedne funkcija. Osnovni blokovi su povezani u
graf kontrole toka. Kompilatori nemaju nikakvih pretpostavki o obliku
grafa kontrole toka (ne zna da li ima if, for, while itd.). Ukoliko
posmatramo svaki od ovih blokova pojedinacno, nemamo mnogo mogucnosti
za optimizacije. Posmatrajmo sledeci primer:
y = a
x = 5
da grana: if y < 0
ne grana:
y = y + 4 y = y - x
z = 4
a = x + 2
b = y + c
Mozemo primetiti da je z = 4 mrtav kod jer se nigde ne koristi u
nastavku. Takodje mozemo primetiti da umesto u liniji a = x + 2
mozemo zameniti x = 5 jer se nigde ranije x nije menjalo.

15.Alokacija i dodela registara (graf zavisnosti,


heuristike bojenja, prosipanje promenljivih u memoriju)
Postoje dve tehnike alokacije registara. Lokalna I globalna:

Lokalna alokacija registara:

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.

Globalna registarska alokacija:

Zadatak registarske alokacije je da pokusa da dodeli svakom cvoru


grafa jedan registar. Kazemo BOJU – registry odgovaraju bojama. Graf
koji posmatramo je RIG.
PRi cemu povezana dva cvora ne smeju da imaju istu boju. Bojenje
grafa je NP kompletan problem, optimalno bojenje se retko kad
pokusava. Ideja je da koristimo heuristiku. Mi idemo brzim algoritmom
koji ce na neki nacin da nam oboji graf. Pps. Da imamo 4 registra.
Heuristika radi tako sto pokusava da pronadje cvor koji ima manje
grana nego sto registara imamo na raspolaganju I izbaci ga iz grafa.
Ako uspemo sve ostalo da obojimo onda cemo uspeti I njega da obojimo
zato sto on ima manje grana nego registara. Ako izbacimo a? Ako obo-
jim sve druge sa 4 boje, to znaci da ce njegova dva suseda biti obo-
jena sa 2 boje I za njega ce postojati slobodna boja, da ima 3 suseda
za njega bi ostala jedna boja, 4 suseda – 4 boje za njega nema boje.
Dakle gadjamo cvorove sa manje grana. IZbacimo njega I upisemo na
neki stek da sam izbacio a, graf mi ostane b, c, d, e, f. Sad opet
jurim cvor koji ima tri grane, imamo b I d, bilo koji uzmem, npr
uzmem b izbacim ga I dolaizm u sledece stanje, na steku imam a b, a
ostanu mi u grafu cvorovi c d e f. Sta se sad gresava? Sad mogu da
uzmem stanje da mi bude a b c, pa onda a b c f e ostane samo cvor d u
grafu I onda imamo a b c f e d I prazan graf. KAD se isprazni graf
znamo da smo uspeli!!! Idemo u nazad I vracamo se sa d I njega bojim
bojom r1 – registar 1, sledeci je e njega bojim r2, idemo u nazad I f
bojimo sa r3 (gledamo sa cim je povezan bio!) nakon toga sledeci na
steku je c, on je povezan sa svima, jedino mogu da ga obojim sa r4,
sad se ubacuje b, mozemo da ga obojimo sa r1 (slobodna boja) I na
kraju dodajemo a on je poslednji na steku, kad vratim a mogu da ga
obojim jedino sa r1 ili r2 sve jedno koja. I ova heuristika je uspela
da pronadje bojenje grafa. Ukoliko se zaglavimo ovim postupkom ne
znaci da ne postoji bojenje grafa! Samo znaci da ga je tesko pronaci.
Mi idemo ovom heuristikom pa ako prodje prodje.

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.

STA BI BILO DA IMAMO 3 registra? Pocetak moze, prvi korak moze da se


uradi. Ali nakon toga nailazimo na problem. SLIKA 41. Ovde je dakle
doslo do zaglavljivanja. U ovom trenutku treba odabrati neki cvor
koji potencijalno nece imati dodeljen registar tokom svog izvrsa-
vanja, koji ce morati biti upisan u memoriju privremeno. Pitanje je
koji od njih. Postoje razne heuristike. Ono sto je kljucni savet je
da se na osnovu strukture control flow grafa, vidi koji se cvorovi ne
javljaju u unutrasnjim petljama. Zasto ne valja da imamo cvor u un-
utrasnjoj petlji stavljati u memoriju? Pps da ce se izvrsavati
stalno! Ako stalno setamo u memoriju pa vracamo nazad to je mnogo
sporo. Dakle heuristika da gledamo da promasimo sve ono sto nije u
petljama u stvari juri retko koriscene promenjive. Drugi kriterijum
je da se gledaju cvorovi koji imaju puno grana, kad njih smestimo
puno toga ce da se oslobodi pa ce postupak da se nastavi produk-
tivnije. Termin se zove SPILLING – prospe nam se nesto iz registara u
memoriju. Postoji nesto sto se zove OPTIMISTICKO BOJENJE, to sto u
ovom trenutku ne mogu da nadjem granu sa manje od tri, ne znaci da ce
zaista ta promenljiva morati da zavrsi u memoriji. Sta moze da se
desi? Mozda je ona povezana sa 3 cvora? I slucajno prilikom bojenja
ja ta tri cvora uspem da obojim sa 2 boje! Onda se radi optimisticko
bojenje. PRonnadje se promenljiva koja je kandidat za spilling, ali
se za sada nista specijalno oko nje ne radi, ona se izbaci I kaze se:
“Ajde da vidimo da li ona stvarno spill?” Da li stvarno necemo moci
da joj dodelimo boju, mozda se desi da imamo srecu, zato se I zove
optimisticko. Izbacim neku I nastaim dalje na neki nacin. Npr izbacim
b a ovde dolazim do cdef, dakle a smo izbacili uspesno na pocetku a b
smo izbacili optimisticno, opet ne znamo sta cemo pa optimisticki
pronadjemo kandidata za spill nek je to npr f, ostane nam cde, pa
onda imamo abfc a u grafu ostane d e pa onda abfcd u grafu ostane e I
na kraju imamo abfcde I prazan graf. E sad idemo u nazad I bojimo!
Pokusamo da rekonstriuisemo nadajuci se da cemo da imamo srecu. Do-
damo za pocetak e I stavimo ga u r1, d dodam I stavim u r2, posle
toga nailazim na c I njega mogu da stavim u r3, I onda naidje f I
vidimo da ne moze! F sad nije potencijalni spil vec pravi spill,
moramo da ga stavimo u memoriju! Sad kad sam odlucio da ga stavim u
memoriju sve ide iz pocetka! Vracamo se na pocetnu fazu gledamo
ponovo control flow graf, I tamo gde smo imali f moramo da dodamo f1
= load(f) iz memorije, imali smo e = d + f, posto f nema svoj regis-
tar, pre nego sto ga upotrebimo moramo da ubacimo ovo ucitavanje.
Nakon toga upotrebim I to je to. Kda imamo f = 2*e u grafu, ne moramo
da stavimo f1 isti registar, nego u neki registar f2 racunamo 2*3 I
onda ga upisemo u memoriju, dakle store na adresu f = f2. Imali smo
jos jedan cvor d = f + c, ovo cemo dakle izmenimo f3 = load f’, I
onda stavimo d = f3 + c. Dakle ista promenljiva f se odjednom
podelila na tri promenljive. Koja je dakle tranformacija koda kad
znamo za koju promenljivu radimo spiling? Pravilo: kad se god nalazi
sa desne strane imamo load, kad god se nalazi sa leve strane imamo
store. Kad smo ovako izmenili kod nastavljamo potpuno istim algorit-
mom kao I ranije pri cemu ponov otrebamo racunati zivost
promenljivih. Zivost promenljivih se ne menja puno. Gde je ziva
promenljiva b I dalje je ziva promenljiva b! Nista nismo dirali sem
f! Gde je god bila ziva promenljiva f nece vise da stoji f vec ce da
stoji f1 f2 ili f3.

Posle mozemo videti da se ovo izvrsi sa 3 boje!


Sta bi se deislo da graf ne mozemo da obojimo sa tri boje? Novi
spilling! Neka nova promenljiva bi morala da ide u memoriju, to je
lose za efikasnost programa. Gde u memoriju? U stek okvir za lokalne
promenljive. Pri ulasku u fju treba da alociramo da svaka lokalna
promenljiva ima svoj proctor u stek okviru ali nece sve promenljive
koristiti proctor. Nama je dovoljno da unapred znamo adrese svake
promenljive zbog generisanja kodova za load I store. Ostale adrese
imamo ali necemo da ih koristimo jer promenljive zive unutar regis-
tara.

16.Izbor instrukcija (zadatak, sabloni, prekrivanje,


pregled algoritama)

Tri osnovne komponente generatora koda su registarska alokacija, se-


lekcija instrukcija I odredjivanje redosleda isntrukcija.

Zadatak selekcije instrukcija je da za svaki troadresni kod odabere


koju asemblersku instrukciju upotrebiti. Ako negde stoji e+1 onda tu
mozemo increment instrukciju. Kad je plus uvek je add to je sablon.
Malo se zapetlja stvar kada se ubace nacini adresiranja. Na nekim
asemblerima imamo mogucnost da kada saabiramo vrednost nekog registra
da sabiramo sa drugim registrom npr add eax ebx, a imamo mogucnost I
insturkcije add eax, 100 na memoriji 100, imamo registarsko I indek-
sno adresiranje. Sad se postavlja pitanje da li se isplati uraditi
mov u neki registar pa onda raditi registarski add ili ovo add eax
100. To zavisi od nekoliko faktora, da li imamo slobodnih registara u
tom trenutku, onda moramo ovo add eax 100. Sa druge strane ako se to
sto se nalazi na 100 cesto koristi u nastavku koda onda se mnogo vise
isplati da ga dovucemo u registar I da ga koristimo. Pps je da je
uvek brze izvrsiti registarsko sabiranje nego registra I necega iz
memorije. Instrukcije koje opisuju add eax 100 same po sebi zauzimaju
vise bajtova (zbog adrese 100), to nam je bitno jer se pre izvrsa-
vanja isntrukcije dovode u kontrolnu jedinicu gde se dekodiraju,
dakle ako dovodimo puno bitova iz memorije – insturkciju iz memorije,
onda moze da se deis da njeno dovlacenje u processor I dekodiranje
trosi vise vremena nego izvrsavanje. Tako da treba izbegavati velike
instrukcije I mnogo je bolje ako uspemo da imamo registarsko regis-
tarsku operaciju. TAko da je ovo pipava prica. Gleda se naredna
upotreba. E sad da bi znao sta se koristi opet nam je potrebna
nekakva analiza toka podataka ponovo. Imamo sablone za sve ove
situacije… Pametni algoritmi koriste te sablone.

17.Rasporedjivanje instrukcija (zadatak, pregled


algoritama)
Odredjivanje redosleda instrukcija – instruction scheduling – problem
odredjivanja permutacija troadresnog koda u okviru osnovnih blokova.
Iammo osnovni blok ono sto ja hocu je da pronadjem permutaciju koja
ce da mi da sto manje I sto jednostavniji generisani kod. Sad je pi-
tanje, da li se za zadati troadresni graf moze odrediti optimalni re-
dosled instrukcija. Ukoliko je graf drvo (moze da se desi da nije
drvo, nego da se neki registar koristi 2x, mozemo da imaamo neki us-
meren graf), drvo ce da bude kad god nemamo zajednicke podizraze, kad
je drvo postoji optimalan algoritam koji ce za svako drvo da pronadje
permutaciju koja ce biti najefikasnija. Kada nije drvo onda postoje
dobre heuristiku koje ne garantuju optimalnost ali nalaze prilicno
dobre rezultate.

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.

You might also like