Odgovori Na Pitanja

You might also like

Download as pdf or txt
Download as pdf or txt
You are on page 1of 39

1. pitanje. Nastanak i namena programskih prevodioca. Veza kompilatora i programskih jezika.

Izazovi u
razvoju kompilatora.

(i)

Već pedesetih godina u troškovima razvoja se primećuje da veći udeo ima softver nego hardver.
Kompajler daju mogućnost da se jedan isti kod koristi na više različitih arhitektura.
Prvi kompajler nastaje za Fortran 1957. godine.

Kompajleri koji su nastali nakon su pratili osnovne principe prvog kompajlera

Program --> Kompajler --> Ciljni kod

Glavni pristupi implementaciji programskih jezika jesu interpretacija i kompilacija.


* Interpreteri ne procesiraju program pre izvršavanja.
* Kompajler iz jednog programa pravi drugi (ciljni kod koji može biti asembler, mašinski itd.)

(ii)

Kompajler treba da:


• omogući jednostavno pisanje programa
• izbegne greške i koristi apstrakcije
• prati intuitivan način razmišljanja
• napravi što bolji izvršni kod
• prepozna i korektno prevodi samo ispravne programe
• uvek uspešno završi svoj rad, bez obzira na vrstu i broj grešaka u izvornom kodu
• bude efikasan
• generiše kratak i efkasan objektni program
• generiše semantički ekvivalentan program izvornom programu

Za kompajlere koji se koriste za razvoj bezbednosno kritičnog servera može da se traži i da budu
formalno verifikovani (COMPCERT)

Takođe, pored softverskih kompajlera postoje i hardverski, koji služe uglavnom da generišu neku
konfiguraciju hardvera potrebnu za rešavanje nekog problema.

(iii)

2. pitanje. Struktura kompilatora

1. Front end, vrši leksičku sintaksnu i semantičku analizu. Transformiše ulazni program u IR
2. Middle end, nezavisan od jezika i ciljne arhitekture i vrši optimizacije nad IR
3. Back end, zavisi od ciljne arhitekture. Takođe odgovoran za arhitekturalno specifične
optimizacije. Obuhvata specifične optimizacije i generisanje koda.
3. pitanje. Pretprocesiranje i linkovanje

• Osnovne faze prevođenja pored kompilacije


• Pretprocesiranje predstavlja pripremu za kompilaciju
• Linkovanje je neophodno da bi se od proizvoda kompilacije napravio izvršni program.

(i) Pretprocesiranje

• Kompilator ne obrađuje tekst programa koji je napisao programer, već tekst koji je nastao
pretprocesiranjem
• Pretprocesiranjem se izvorni kod organizuje u više ulaznih datoteka.
• Objedinjava izvorni kod različitih datoteka u jedinice prevođenja koje šalje kompilatoru. Na
primer .c datoteke koje sadrže datoteke zaglavlja
• Pretprocesor analizira samo pretprocesorske direktive. Primer su #include i #define u C-u.

(ii) linkovanje

• Linkovanje predstavlja proces kreiranja jedinstvene izvršne datoteke na osnovu više


objektnih modula koji su nastali kompilacijom izvornog koda ili su objektni moduli koji
sadrže mašinski kod i podatke neke biblioteke.
• Pored statičkog postoji i dinamčiko linkovanje (za vreme izvršavanja programa)
• Kompilaciju i linkovanje je moguće razdvojiti opcijama kompajlera.

4. pitanje. Leksička analiza

Leksika je podoblast sintakse koja se bavi opisivanjem osnovnih gradivnih elemenata jezika.
U okviru leksike definišu se reči i njihove kategorije. U programskom jeziku reči se nazivaju
lekseme a kategorije tokeni.

Pojedinačni karakteri se grupišu u nedeljive celine lekseme koje predstavljaju osnovne leksičke
elemente i pridružuju im se tokeni koji opisuju leksičke kategorije kojima te lekseme pripadaju.

Tokeni mogu da budu: identifikatori, ključne reči, komentari, bilo šta što smo videli recimo u
lexer.lex fajlovima kad smo programirali.

* Programski jezik C sadrži više od 100 različitih tokena.

(ii) Leksička analiza


• Leksička analiza je proces izdvajanja leksema i tokena iz niza ulaznih karaktera
• LA vrše moduli kompilatora koji se nazivaju lekseri ili leksički analizatori
• Lekseri najčešće rade na zahtev sintaksnog analizatora tako što mu SA traži token svaki put
kada mu zatreba.
• Tokenima mogu biti pridruženi i dodatni atributi. Recimo svakom tokenu identifikator
može biti pridružen pokazivač na mesto u memoriji gde se čuva vrednost promenljive sa
tim nazivom. Brojevnim konstantama može biti pridružena vrednost i slično.

** Mehanizam za izdvajanje lekseme sa ulaznog teksta se naziva konačni automat

// Ovde se može pisati nešto i o regex-u. Na slajdovima je pominjala regex.


5. pitanje. Sintaksička analiza

SA definiše relacije između elemenata jezika, time pružajući strukturne opise ispravnih niski
jezika.
Sintaksa definiše načine na koje pojedinačne reči mogu da kreiraju ispravne "rečenice" jezika.
Sintaksa se bavi samo formom i strukutrom jezika bez bilo kakvih razmatranja značenja
(semantike).

Sintaksička stuktura rečenica ili programa se obično predstavlja preko sintaksnog stabla.
Sintaksom programa se obično bavi deo programskog prevodioca koji se naziva parser
Rezultat njegovog rada je sintaksno stablo koje se isporučuje daljim fazama.

Sintaksa jezika se opisuje gramatikama.


Za opis sintaksičkih konstrukcija programskih jezika uglavnom se koriste kontekstno-slobodne-
gramatike.
Sve što je moguće opisati regularnim izrazima moguće je opisati i KSG-om.

Kontekstno-slobodne-gramatike određuje skup pravila.


• Svako pravilo ima levu i desnu stranu
• Sa leve strane se nalaze neterminali a sa desne terminali.
• Svakom pomoćnom simbolu je pridružena sintaksička kategorija. Jedan od pomoćnih
simbola je istaknut, on se naziva aksioma (početni simbol)
• Niska je opisana gramatikom ako se može dobiti krenuvši od početnog simbola (aksiome)
zamenjujući u svakom koraku pomoćne simbole desnim stranama pravila.

* U KSG-ama sa leve strane se nalazi samo jedan neterminal.

Na osnovu gramatike jezika formira se potisni automat na osnovu kog se jednostavno


implementira program koji vrši sintaksičku analizu. To se obično vrši automatski koristeći generator
parsera.

** Sad ovde treba da idu neke forme BNF EBNF i sl. ali kapiram da to ne dolazi uz ovo pitanje.
6. pitanje. Semantička analiza Formalna i neformalna semantika. Semantičke greške. Semantička
upozorenja, primeri.

• Semantika pridružuje značenje ispravnim jezičkim konstrukcijama.


• Obično je značajno teže definisati semantiku nego sintaksu.
• Semantika može da se opiše formalno i neformalno. Često se zadaje samo neformalno.
Uloga neformalne semantike je čisto da programer može da razume kako se program
izvršava pre njegovog pokretanja.
Primer: " if (a < b) a++; " se neformalno opisuje "ukoliko je vrednost promenljive a manja
od vrednosti promenljive b, onda uvećaj vrednost promenljive a za jedan"

(i) Formalna semantika

• Operaciona semantika, denotaciona semantika, aksiomatska semantika


• Formalne semantike se koriste za izgradnju alata koji se dalje koriste za naprednu
semantičku analizu softvera.
• Ovi alati se mogu koristiti kao dopuna semantičkoj analizi koju vrše kompajleri.

(ii) Semantičke greške

Semantičke greške se oktrivaju nakon leksičke i sintaksne analize.


Primeri grešaka su recimo korišćenje nedefinisane promenljive, nedefinisanog simbola, klase ili
funkcije. Simboli definisani više puta u istom dosegu, npr. dva puta definisana promenljiva.

Semantičke greške u tipovima:


• Nepodržana konverzija iz jednog tipa u drugi
• Indeksiranje skalara
• Indeksiranje indeksom koji nije celobrojnog tipa
• U nekim jezicima if naredba zahteva boolean vrednost

Greške u pozivima funkcije:


• Identifikator funkcije mora biti definisan kao funkcija.
• Broj argumenata se mora poklapati sa brojem argumenata prototipa.
• Svako return u telu funkcije mora imati vrednost tipa povratne vrednosti funkcije.

(iii) Semantička upozorenja

Postoje različite vrste semantičkih upozorenja, važenja nekih semantičkih pravila se negde mogu
proveriti pre izvršavanja programa a negde to nije moguće. (Recimo deljenje nulom ne bi bilo
moguće uhvatiti pre izvršavanja u nekim slučajevima)

• Rezultati semantičkih provera često se prikazuju korisniku kao upozorenja


• Mnoge semantičke provere i u pozorenja nastaju kao rezultat iskustva. Na primer ako se
uvede neka promenljiva koja se ne koristi, onda je verovatno programer pogrešio slučajno.
Još jedan primer ovakve greške je naredba dodele umesto naredbe poređenja, jako česta
greška.

Što je ispravnost aplikacije bitnija i kritičnija to je detaljnija semantička analiza i provere.


Najčešće se izbor provera može kontrolisati opcijama kompajlera.
7. Uloga medukoda i generisanje medukoda. Primer gcc.

Većina kompilatora prevodi sintaksičko stablo dopunjeno i provereno nakon semantičke analize u
odeđeni međukod. Međukod se nakon toga analizira i optimizuje i na osnovu njega se kasnije gradi
rezultujući asemblerski ili mašinski kod.

Čemu ovo služi?


• Pojednostavljivanje optimizacija. Rad sa međukodom čini izvršavanje pojedinih
optimizacija mogućim ili lakšim.
• Da bi imali više "prednjih delova" za isti zadnji deo. Svaki front end prevodi source kod u
generički jezik.
• Da bi imali više "zadnjih delova" iz jednog "prednjeg dela"

(ii) Generisanje međukoda

• Veoma je teško dizajnirati IR jezik.


• Potrebno balansirati potrebe jezika niskog i jezika visokog nivoa.
• Ukoliko je nivo previsok, nije moguće implementirati neke optimizacije
• Ukoliko je nivo prenizak, nije moguće koristiti znanje visokog nivoa da se izvrše neke
agresivne optimizacije.
• Kompajler često imaju više od jedne međureprezentacije.

U okviru gcc-a postoji više međureprezentacija.


Neki IR-ovi visokog nivoa su Java bytecode, CPython bytecode i Microsoft CIL

IR visokog nivoa:
• Zadržava strukturu programskog jezika visokog nivoa
• Dozvoljava kompilacije na ciljanim mašinama
• Dozvoljava JIT kompilaciju ili interpretaciju.

Različiti oblici međureprezentacije:


1. Grafička reprezentacija (npr. sintaksna stabla )
2. Troadresna reprezentacija ( trojke )
3. Reprezentacija virtualne mašine ( stack machine code )
4. Linearna reprezentacija ( postfiks notacija )

Najčešći oblik je troadresni kod, gde se javljaju dodele promenljivama tako da sa desne strane
dodele ima najviše jedan operator. Naziv nosi jer se u svakoj instrukciji navode "adrese" najviše dva
operatora i rezultata operacije.

Naredbe kontrole toka se uklanjaju i svode na uslovne i bezuslovne skokove.

(iv) Primer GCC

GCC koristi tri najvažnije međureprezentacije da predstavi program prlikom kompilacije


1. GENERIC ( nezavisna od jezika, svaki gcc podržan jezik se može prevesti na ovu
međureprezentaciju)
2. GIMPLE ( troadresna međureprezentacija nastala od GENERIC tako što se svaki izraz svodi
na troadresni ekvivalent) Koristi SSA (static single assignment)
3. RTL (Register transfer language)

GENERIC -> High GIMPLE -> SSA -> Low GIMPLE -> RTL -> Machine Code
8. Optimizacije medukoda. Uloga i primeri.

Optimizacija podrazumeva poboljšanje performansi koda, zadržavajući pri tom semantičku


ekvivalentnost sa polaznim.
Fazi optimizacije prethodi faza analize na osnovu koje se donose zaključci i sprovode optimizacije.

Cilj: Unaprediti IR generisan prethodnim koracima da bi se bolje iskoristili resursi.

Razlozi optimizacije:
• Generisanje IR-a uključuje redundatnost u naš kod.
• Programeri su lenji. (Recimo obično postoji kod u petlji koji ne mora da bude u petlji )

Optimizacija se najčešće odvija na dva nivoa:


• Optimizacija međukoda se generiše na početku faze sinteze i podrazumeva optimizacije
koje ne uzimaju u obzir ciljanu arhitekturu.
• Optimizacija ciljnog koda se izvršava na kraju sinteze i zasniva se na detaljnom
poznavanju ciljne arhitekture, asemblerskog i mašinskog jezika na kom se izvršava ciljni
program.

Primeri:
• Constant folding - Konstantni izrazi se mogu izračunavati
• Constant propagation - Izbegava se upotreba promenljivih čija je vrednost konstanta
• Strength reduction - Operacije se menjaju onim za koje se smatra da će se izvršiti brže
• Copy propagation - Izbegava se uvođenje promenljivih koje samo čuvaju vrednosti nekih
postojećih promenljivih.
• Dead code elimination - Izračunavanja vrednosti promenljivih koje se dalje ne koriste se
eliminišu.
• Optimizacija petlji - Jedna od najznačajnijih. Uključuje izbacivanje konstantnih izraza van
tela petlje.

9. Generisanje koda. Izazovi. Cisc i risc arhitekture.

Tokom generisanja koda optimizovani međukod se prevodi u završni asemblerski tj. mašinski kod.

Faze generisanja koda:


1. Faza odabira instrukcija (određuje se kojim mašinskim instrukcijama se modeluju troadresne
instrukcije)
2. Faza alokacije registara (određuje se lokacija na kojoj će se svaka promenljiva skladištiti)
3. Faza raspoređivanja instrukcija (ada se odreduje redosled instrukcija koji doprinosi
kvalitetnijem iskorišćavanju protočne obrade i paraleizacije na nivou instrukcija)

Obično se prvo vrši faza odabira instrukcija, a faza raspoređivanja nekad može ići i pre i posle faze
alokacije registara.

Izazovi generisanja koda

• Generisanje optimalnog koda je neodlučiv problem ( u praksi se koriste napredne heurističke


tehnike )
• Ispravnost
◦ Najbitniji kriterijum za generator koda
◦ Posebno je važno dizajniranje generatora koda tako da on može biti lako implementiran,
testiran i održavan.

Više prolaza kroz IR

• Ulaz u generator koda je IR izvornog programa zajedno sa tablicama simbola koje služe za
utvrđivanje run-time adresa objekata koji se koriste po imenu u okviru IR-a
• Generatori koda razdvajaju instrukcije u "basic blocks"
• U okviru faza optimizacije i generisanja koda obično postoje višestruki prolazi kroz IR koji
se izvršavaju pre generisanja ciljnog programa.

CISC (Complex instruction set computer)

• Cisk arhitekture procesora karakteriše bogat skup instrukcija


• Broj instrukcija se smanjuje ugradnjom više operacija u jednu instrukciju
◦ Primer, jedna instrukcija može učitavati nešto iz memorije, menjati i vratiti u memoriju.
• Instrukcije mogu biti različitih dužina
• Svi operandi mogu biti registri, ali samo jedan može biti memorijska lokacija.
• CISC procesori se koriste uglavnom na ličnim računarima
• CISC procesori imaju više različitih načina adresiranja, od kojih su neki veoma kompleksni
• CISC procesori obično nemaju veliki broj registara opšte namene

RISC (Reduced instruction set computer)

• Na osnovu analiza je utvrđeno da većina kompleksnih instrukcija jako retko koristi i da to ne


opravdava podizanje cene hardvera.
• Treba ubrzati ono što se najviše koristi, tako je nastao RISC - arhitektura sa procesorom
smanjenog skupa instrukcija

Risc arhitektura procesora:


• se zasniva na pojednostavljenom i smanjenom skupu instrukcija koji je visoko optimizovan
• zbog jednostavnosti instrukcija, potreban je manji broj tranzistora za proizvodnju procesora,
pri čemu će se instrukcije brže izvršavati
• redukovanje skupa instrukcija međutim umanjuje efikasnost pisanja softvera za procesore,
što ne predstavlja problem u slučaju automatskog generisanja koda kompajlerom.
• Ne postoje složene instrukcije koje pristupaju memoriji, već se rad sa memorijom svodi na
load i store instrukcije.
• Najveća prednost je protočna obrada, koja se lako može implementirati.
• Zbog protočne obrade RISC arhitektura ima veliku prednost u performansama u odnosu na
CISC arhitekture.
• RISC procesori se uglavnom koriste za real time aplikacije.
10. Izbor instrukcija. Izbor registara. Rasporedivanje instrukcija.

Izbor instrukcija

Kod generator mora da mapira IR program u sekvencu koda koji može da bude izvršen na ciljanoj
arhitekturi. Kompleksnost mapiranja zavisi od:
• Nivoa apstrakcija / Preciznosti IR-a
◦ Ukoliko je IR visokog nivoa, kod generator moze prevoditi svaku IR isntrukciju u
sekvencu instrukcija, takav kod kasnije verovatno mora da se optimizuje
◦ Ukoliko je IR niskog nivoa, onda se očekuje da takav kod bude efikasan
• Priroda instrukcija arhitekture
◦ Uniformnost i kompletnost skupa instrukcija su bitni faktori
◦ Na nekim mašinama recimo floating point se rešava sa odvojenim registrima (loše)
• Željeni kvalitet generisanog koda
◦ Brzina instrukcija je bitan faktor
◦ Ako nas nije briga za efikasnost ciljanog programa, odabir instrukcija je trivijalan

Izbor registara.

Tokom faze registarske alokacije odeđuju se lokacije na kojima će biti skladištene vrednosti svih
promenljivih koje se javljaju u međukodu. Cilj je da što više promenljivih bude skladišteno u
registrima (zbog brzine pristupa memoriji)
*Dve promenljive mogu biti smeštene u isti registar ako im se životni vek ne preklapa.

Problem korišćenja registara je obično podeljen u dva problema:


• Alokacija registara - tokom koje se biraju skupovi promenljivih koje će biti čuvane u
registrima u svakoj tački programa.
• Dodela registara - tokom koje se biraju konkretni registri za pohranjivanje promenljivih.

Čak i sa jednoregistraskim mašinama zadatak pronalaženja optimalnih dodavanja registra je


komplikovan.

• Izbor registara je NP kompletan problem.


• Problem je još komplikovaniji jer pojedine arhitekture zahtevaju neke konvencije da se
ispoštuju po pitanju korišćenja registara.

Raspoređivanje instrukcija

Faza raspoređivanja instrukcija pokušava da doprinese brzini izvršavanja tako što promeni redosled
instrukcija u programu. Na primer, neki rasporedi instrukcija zahtevaju manji broj registara za
privremene rezultate.

Izbor najboljeg redosleda je u opštem slučaju NP kompletan problem.


Najjednostavnije rešenje jeste da se uopšte ne menja redosled instrukcija koje je dao IR kod.

Savremeni procesori mogu da izvrše više instrukcija tokom jednog otkucaja sistemskog sata.
Primer: Dok se jedna instrukcija dovlači iz memorije u procesor, druga se dekodira,
treća se već izvršava, a četvrtoj se već skladište rezultati.
11. Jednoprolazni i višeprolazni kompilatori

Analiza se može obaviti u jednom ili više prolaza.


Skeniranje i parsiranje se može odraditi u jednom prolazu.

Neki kompajleri kombinuju skeniranje, pasrsiranje, semantičku analizu i generisanje koda u jednom
prolazu. Takvi kompajleri se nazivaju jednoprolazni kompajleri.

Većina kompajler ipak prolazi kroz kod više puta i to su višeprolazni komapjleri.

Neki jezici su dizajnirani tako da podrže jednoprolazne kompajlere (C i C++ recimo)


Neki jezici zahtevaju višeprolazne komajlere (Java)
Većina modernih komapjlera koristi veoma veliki broj prolaza kroz kod.

Pravila dosega u višeprolaznim kompajlerima:


• Prvi prolaz: kompletno parsiranje ulaznog koda i kreiranje ASTa
• Drugi prolaz: prolazak kroz AST i skupljanje informacija o klasama
• Treći prolaz: prolazak kroz AST i provere raznih osobina
* Prolazi se mogu kombinovati

12. LLVM osnovne informacije. Značaj i mogućnosti.

• Projekat llvm sastoji se iz biblioteka i alata koji zajedno čine veliku komajlersku
infrastrukturu.
• Započet je kao istraživački rad na Univerzitetu Ilionis 2000. godine, sa ciljem proučavanja
tehnika kompajliranja i kompajlerskih optimizacija.
• Ideja: skup modularnih i ponovno iskoristivih kompajlerskih tehnologija, čiji je cilj podrška
statičkoj i dinamičkoj kompilaciji proizvoljnih programskih jezika.

Od prve verzije 2003. godine llvm je prerastao u projekat koji se sastoji iz velikog broja
podprojekata koji se koriste u:
• Industriji, na primer Apple, ARM, Nvidia...
• Akademiji, samo na matfu :P

Dobio ACM Software System Award

LLVM je sveobuhvatni naziv više projekata koji zajedno čine potpun kompajler:
• Prednji deo
• sredisnji deo
• zadnji deo
• optimizatore
• asemblere
• linkere
• i druge komponente...

Projekat je napisan u C++


Clang/Clang++ se često koristi kao sinonim za LLVM kompajler.
Clang ima odlične karakteristike u poređenju sa kompajlerima kao što su gcc i icc. Obično Clang
ima brže vreme kompilacije od ostalih kompajlera.

• LLVM implementira kompletan tok kompilacije:


• Front-end, leksička, sintaksička i semantička analiza — npr alat Clang
• – Middle-end, analize i optimizacije — npr alat opt
• – Back-end, različite arhitekture — npr alat llc

(ii) Mogućnosti
Na primer, korisnik može:
• da kreira svoj front-end, i da ga poveže na LLVM, koji će mu dodati middle-end i back-end
• za izabran postojeći front-end/back-end da isprobava promene na nivou middle-end-a
• za novu arhitekturu obezbedi back-end, i da koristi Clang i postojeci middle-end

13. LLVM projekti.

Kompajlerska infrastruktura sa velikim brojem pomoćnih alata, podržanih arhitektura, sa


interfejsima ka ostvarivanju različitih analiza i optimizacija.

LLVM core libraries:


• Obezbeđuju moderni optimizator koji ne zavisi od arhitekture. Dobro dokumentovane
biblioteke daju mogućnost da se jako lako implementira LLVMov optimizator i generator
koda.

Clang
• Clang je LLVMov C/C++/Objective-c kompilator, koji ima za cilj
◦ Efikasnu kompilaciju
◦ Veoma detaljnu dijagnostiku
◦ Obezbeđivanje platforme za izgradnju različitih alata koji rade na nivou izvornog koda.

Ostali LLVM projekti:


• lldb -llvm debager
• libc++ -standardna biblioteka
• compiler-rt -podrška za različite run time izazove
• MLIR -nova međureprezentacija
• polly -optimizator petlji
• lld -llvm linker
• ima još...

14. LLVM prednji deo.

Predstavlja prevođenje i analizu izvornog koda programa zapisanih u višim programskim jezicima u
LLVM-ovu međureprezentaciju.
Prevođenje obuhvata leksičku, sintaksičku i semantičku analizu i završava se fazom generisanja
llvom-og međukoda.
Clang je frontend za C jezike. Frontendi za ostale jezike nisu deo LLVMa ali postoji podrška za
značajnije jezike. (Haskell, JavaScript itd.. itd..)

Prve verzije llvma su kao frontend koristile gcc, odnosno modifikovanu verziju llvm-gcc. Da bi se
koristio ovaj alat bilo je potrebno skinuti i izgraditi ceo modifikovan gcc. Zahtevno i komplikovano.

Nakon toga je pokušano sa projektom Dragon Egg koji je bio dodatak za gcc. Nije bilo potrebno
skidati i izgrađivati llvm-gcc

Danas za tim nema više potrebe jer postoji Clang.

Clang:
• Memorijski efikasan, ispisuje jasne poruke o upozorenjima i greškama
• Izražajna dijagnostika
• Pruža čist API tako da se on može koristiti i u drugim projektima.

Ceo sistem llvma je organizovan kao skup biblioteka.

15. LLVM srednji deo. LLVM-ov medukod.

Središnji deo obuhvata skup mašinski nezavisnih optimizacija koje se izvršavaju nad LLVMovom
međureprezentacijom.
LLVMova međureprezentacija predstavlja sponu između prednjeg i zadnjeg dela komajlera.
Dizajn LLVM IR je bio od posebnog značaja za uspeh projekta.

LLVMov međukod može biti predstavljen pomoću narednih, međusobno ekvivalentnih formi:
• memorijska reprezentacija - implicitna forma međukoda
• bitkod reprezentacija - smeštena u datotekama *.bc
• pseudo-asemblerska reprezentacija - lako čitljiva, smeštena u *.ll datotekama

Funkcije konceptualno odgovaraju funkcijama programskog jezika C.


Naredni entitet u hijerarhiji predstavljaju osnovni blokovi (eng. basic blocks) od kojih je sačinjena
svaka funkcija. Osnovni blok (eng. basic block) predstavlja najduži niz sekvencijalnih instrukcija
koje se završavaju sekvencijalno.

Hijerarhijski najniže entitete predstavljaju same instrukcije.


Instrukcije su slične RISC instrukcijama ali sa ključnim dodatnim informacijama koje mogućavaju
efikasnije analize.

Najvažnije jezičke osobine međureprezentacije:


• SSA (Static Single Assignment)
• Troadresne instrukcije
• Poseduje ograničen broj registara

Lokalni identifikatori u llvmu imaju prefiks % a globalni @


Neke od instrukcija: alloca, load, store, ret, add, sub ...

file.c -> CLANG -> file.bc -> OPT -> file.bc -> llc -> file.s

16. LLVM srednji deo. Alat opt i LLVM prolazi.


LLVM srednji deo je rešen u prethodnom pitanju.

Alat opt vrši analizu i optimizaciju koda. Kao ulazni fajl očekuje IR kod i onda pokreće specifičnu
optimizaciju ili vrši analizu koda, u zavisnosti od ulaznih opcija.

Ukoliko opcija -analyze nije zadata, opt pokušava da proizvede optimizovani izlazni kod.
Postoje razne vrste optimizacija.

• Svaka optimizacija je zadata u vidu prolaza kroz kod. Moguće je menjati redosled
optimizacija.
• Da bi se pokrenula neka konkretna optimizacija navodi se ime odgovarajućeg prolaza.
• Redosled pojavljivanja prolaza u komandnoj liniji određuje redosled izvršavanja
• Neke optimizacije zahtevaju da su se pre njih odradile posebne analize i da su prikupljeni
potrebni podaci.

Prolazi mogu da rade analizu ili optimizaciju.

Postoje prolazi koji su bitni za dobijanje raznih informacija neophodnih za neke druge prolaze. Ovi
prolazi se zovu Utility passes.

(ii) LLVM prolazi.

Da bi napisali neku optimizaciju/prolaz potrebno je da znao:


• šta naš prolaz treba da uradi
• na koji način zavisimo od analiza programa koje vrše drugi prolazi
• na koji način utičemo na već izvršene analize programa.

Svi prolazi su podklase apstraktne klase Pass.


Svi prolazi imaju svoja pravila kako mogu a kako ne mogu da se ponašaju i šta im je dozvoljeno.

U zavisnosti od nivoa na kojem operiše, prolaz može da bude


• ModulePass,
• CallGraphSCCPass, (strongly connected component, za bottom up prolaske)
• FunctionPass, (initi i finalize rade nad modulom)
• LoopPass,
• RegionPass, single entry-single exit regija u funkciji
• BasicBlockPass (init i finalize rade nad funkcijom)
• MachineFunctionPass — prolaz koji se ne korsiti u okviru opt-a već za generisanje ciljanog
koda

Registrovanje poziva je potrebno da bi prolaz mogao da se poziva po imenu.

Odnosi između prolaza:


• Prolaz može da menja IR ili samo da vrši analizu nad IR
• Prolaz može da zahteva informacije koje obezbeđuju neki drugi prolazi. Zavisnosti između
prolaza moraju da budu eksplicitno navedene.
• Može se zahtevati skup neophodnih prolaza koji moraju da prethode našem prolazu.

Bitno je da se nakon svakog prolaza ostavi IR u ispravnom stanju.

17. LLVM zadnji deo.


Zadnji deo predstavlja generisanje mašinskog koda za navedenu ciljnu arhitekturu, na osnovu
prosleđenog LLVMovog međukoda, vrši se u zadnjem delu infrastrukture.

*Takođe u ovom delu se vrše mnoge mašinski zavisne optimizacije.


Osnovni alat zadnjeg dela je llc, to je kompajler koji prevodi LLVM bitcode u asemblerski fajl.

Da bi se ostvarila podrška za novi hardver, potrebno je:


• Poznavati llvm-IR, jer zadnji deo počinje obradu nad IR
• Poznavati tehnike pisanja prolaza
• poznavati td format:
◦ td format precizira način definisanja karakteristika ciljne arhitekture sa ciljem bržeg i
jednostavnijeg automatskog generisanja odgovarajućih C++ klasa i metoda
◦ Sintaksa liči na generičko programiranje u C++
• Poznavati algoritme koji se koriste u fazi generisanja koda: izbor instrukcija, redosled
instrukcija, SSA-zasnovane optimizacije, alokacija registara, kasne optimizacije na
mašinskom kodu i emitovanje koda.
◦ Za svaku od prethodnih faza postoji skup klasa koje treba poznavati i predefinisati.
Imena klasa obično govore o nameni klase (npr. TargetMachine)
18. Semantička analiza. Ime, doseg i tabela simbola. Operacije nad tabelom simbola.

Razne vrste semantičkih grešaka:


• Da li su identifikatori deklarisani na mestima na kojima se javljaju?
• Da li se poštuju navedeni tipovi podataka?
• Da li su odnosi nasleđivanja korektni?
• Da li se klase definišu samo jednom?
• I druge...

Validnost vs korektnost (Validan je ako nema grešaka, korektan je ako radi ono što treba da radi)

Izazovi semantičke analize:


• Odbaciti što više nekorektnih programa
• Prihvatiti što više korektnih
• Uraditi to brzo
• Prikupiti i druge bitne informacije
◦ Koju promenljivu označava koja varijabla
◦ Interno predstavljanje hijerarhije nasleđivanja
◦ Izbrojati koliko promenljivih je u dosegu u svakoj tački programa

Razmatramo dve vrste semantičke analize:


• Scope-Checking (provera dosega) - koji identifikator se odnosi na koji konkretan objekat.
• Type-Checking (provera tipova) - Da li promenljive imaju validne tipove?

(ii) Provera dosega

Isto ime u programu može da referiše na više različitih stvari. (ne promenljivih jer nije isključivo)

Doseg nekog objekta je skup lokacija u programu gde se može referisati na taj objekat preko
uvedenog imena.
Uvođenje nove promenljive u doseg može da sakrije ime neke prethodne promenljive.

Za praćenje vidljivosti promenljivih koristimo tabelu simbola.


Tabela simbola je preslikavanje koje za svako ime određuje čemu to ime pripada.
Kako se izvršava semantička analiza, tabela simbola se menja i osvežava.

(iii) Operacije nad tabelom simbola

Tabela simbola je tipično implementirana kao stek kataloga (mapa)


Svaka mapa odgovara jednom konkretnom dosegu.

Osnovne operacije su:


• push scope
• pop scope
• insert symbol
• lookup symbol

Da bi se obradio deo programa koji kreira neki doseg potrebno je:


• Ući u taj doseg
• Dodati sve deklarisane promenljive
• Obraditi telo bloka/funkcije/klase
• Izaći iz dosega
Veliki deo semantičke analize se vrši na ovaj način. Rekurzivnim prolaskom kroz AST.

*Svaki doseg čuva pokazivač na svog roditelja, ali ne i obrnuto.


Iz svake tačke programa tabela simbola izgleda kao stek.

19. Doseg i tabela simbola u OOP. Odredivanje dosega kod nasledivanja. Razrešavanje
višeznačnosti

Doseg u okviru objektno orijentisanog programiranja.


• Doseg izvedene klase obično čuva link na doseg njene bazne klase
• Traženje polja klase prolazi kroz lanac dosega i zaustavlja se kada se pronađe odgovarajući
identifikator ili kada se pojavi semantička greška

(ii) Kod nasleđivanja potrebno je održavati još jednu tabelu pokazivača koja pokazuje na stek
dosega.
Kada se traži vrednost u okviru specifičnog dosega počinje se sa pretragom u tom konkretnom
dosegu.
Neki jezici kao C++ omogućavaju skakanje do proizvoljne bazne klase ( primer " cout << A::x " )

(iii) Pojednostavljena pravila dosega u C++


• U okviru klase pretraži celu hijerarhiju da vidiš koji skupovi imena se tu mogu naći
• Ako se pronađe samo jedno odgovarajuće ime, onda se tu pretraga prekida
• Ako se pronađe više odgovarajućih imena, onda je pretraga dvosmislena i mora se zahtevati
razrešavanje.
• U suprotnom poćni ponovo pretragu ali van klase.
20. Odredivanje dosega. Dinamički dosezi.

Neki jezici koriste dinamičko određivanje dosega, koje se sprovodi u fazi izvršavanja: ime odgovara
varijabli sa tim imenom koja je najbliže ugnježdena u fazi izvršavanja.

Primeri jezika sa dinamičkim dosezima su Perl i Common Lisp


Implementacija dinamičkih dosega uključuje čuvanje tabele u fazi izvršavanja.
Obično je to manje efikasno od statičkih dosega jer komapjleri ne mogu da hardkoduju lokacije
promenljivih već imena moraju da se razreše u fazi izvršavanja.

** nisam siguran na šta se odnosi Odeđivanje dosega, da li na generalno određivanje ili je to ovo što
je ovde napisano.

21. Pravila za odredivanje tipova u izrazima.

Notacija tipova zavisi od programskoj jezika ali uključuje:


• Skup vrednosti
• Skup operacija nad vrednostima

Greška u radu sa tipovima se javlja kada se primenjuje operacija nad vrednostima koje ne
podržavaju tu operaciju.

Vrste provere tipova:


• Statička provera tipova:
◦ Analiziraj program u fazi kompilacije kako bi pokazao da nema grešaka u tipovima
◦ Nikad ne dozvoli da se nešto desi u fazi izvršavanja
• Dinamička provera tipova:
◦ Proveri operacije u fazi izvršavanja, neposredno pre konkretnog izvršavanja
◦ Preciznije od statičkog, ali manje efikasno
• Bez analize tipova

Dinamičko određivanje tipova olakšava pisanje prototipova ali ima više grešaka.

Pravila koja definišu šta je dozvoljena operacija nad tipovima formiraju sistem tipova.
U jako tipiziranim jezicima svi tipovi moraju da se poklapaju.
U slabo tipiziranim jezicima mogu se desiti greške u tipovima u fazi izvršavanja.

Jako tipizirani jezici su robusni, ali slabo tipizirani jezici su obično brži.

Zaključivanje tipova:
• Dva osnovna koraka statičkog zaključivanja:
◦ Zaključivanje tipa za svaki izraz na osnovu komponenti izraza
◦ Potvrđivanje da tipovi izraza u određenom kontekstu se poklapaju sa očekivanim
◦ Ovi koraci se obično kombinuju u jednom prolazu

Jednostavna pravila zaključivanja:


• Ako je x promenljiva tipa t , izraz x ima tim t
• Ako je e celobrojna konstanta onda e ima tip int
• Ako operandi e1+e2 su tipa int onda i sam izraz ima tip int
• Ovakav zapis treba da se formalizuje i skrati
Formalan zapis:
Aksiome i pravila zaključivanja mogu da se zapišu na sledeći način
• Preduslov / Postuslov

22. Tipovi i nasledivanje. Tip null.

Osobine nasleđivanja:
• Svaki tip nasleđuje samog sebe.
• Ako A nasleđuje B i B nasleđuje C onda i A nasleđuje C
• Ako A nasleđuje B i B nasleđuje A onda su A i B istog tipa
Osobine konvertovanja:
• Ako A nasleđuje B, onda se objekat klase A može konvertovati u objekat klase B
• Svaki tip se može konvertovati u samog sebe
• Ako se A može konvertovati u B i B u C onda može i A u C.
• Ako se A može konvertovati u B i B u A onda su A i B istog tipa

Ako je A osnovni tip ili niz onda se on može konvertovati samo u samog sebe.

(ii) Tip Null

Definišemo novi tip koji odgovara literalu null i zovemo ga null tip.
Definišemo da važi null tip <= A, za svaki klasni tip A
Ovaj tip obično nije dostupan programerima već se koristi samo interno u sistemu tipova
Mnogi programski jezici imaju ovakav tip u svom sistemu tipova

23. Odredivanje tipova kod ternarnog operatora.

Terarni operator ? : izračunava izraz i vraža jednu od dve vrednosti.


Može se koristiti sa osnovnim tipovima ili sa nasleđivanjem klase.
Minimalno gornje ograničenje:
• Gornje ograničenje tipova A i B je tip C tako da je A <= C i B <= C
• Minimalno gornje ograničenje tipova A i B je tip C tako da važi:
◦ C je gornje ograničenje od A i B
◦ Ako je C2 gornje ograničenje od A i B onda ne važi da je C2 < C
• Minimalno gornje ograničenje ne mora da bude jedinstveno.
• Najmanje gornje ograničenje mora da bude minimalno gornje ograničenje, ali ne važi
obrnuto.

Ispravno formalno pravilo

(S⊢ cond: bool, S⊢ e1: T1, S⊢ e2: T2, T is minimal upper bound of T1 and T2)
---------------------------------------------------------------------------------------------
S ⊢cond ? e1 : e2 : T

24. Pravila za odredivanje tipova u naredbama.

Da li su tipovi izraza u okviru naredbi ispravni?


• Da li if ima dobro formiran uslov?
• Da li return naredba vraća adekvatan tip?
• I drugi problemi...
Ispravnost naredbi

Ideja: Proširiti sistem izvođenja zaključaka tako da modeluje naredbe

Kažemo da je: S ⊢ WF(stmt)


ako je naredba stmt dobro formirana (WF = "well formed") u dosegu S.

Sistem tipova je zadovoljen ako za svaku funkciju f sa telom B koji je u dosegu S možemo da
dokažemo da važi: S ⊢ WF(B)

Sad ovde treba navesti one formalne definicije pravila za petlje, break naredbu, if naredbu, while
naredbu itd... To sve treba pogledati na 04_semanticka_analiza_tipovi.txt nas tranici 28.

25. Tipovi i propagiranje greške.

Kako proveriti da li je program ispravno formiran?


• Proći rekurzivno kroz stablo i za svaku naredbu proveriti tip podizraza koji sadrži:
◦ Prijavi grešku ako ne može tip da se dodeli izrazu (recimo true + 2)
◦ Prijavi grešku ako je pogrešan tip dodeljen izrazu

Propagiranje greške:
• Statička greška tipova se dešava kada ne možemo da dokažemo da izraz ima odgovarajući
tip
• Greške u tipovima se lako propagiraju. Na primer, ako ne možemo da dokažemo tip od e1
onda ne možemo da dokažemo ni tip od e1 + e2 i tako dalje

Kako to prevazići?

Tip Error:
• U sistem tipova uvodimo novi tip koji predstavlja grešku
• Ovaj tip je manji do svih drugih tipova i označava se sa ꓕ.
• Nekada se zove i bottom tip.
• Po definiciji važi ꓕ < A, za svako A.
• Kada otkriješ tip error pretvaramo se da smo dokazali tip ꓕ.
• Potrebno je unaprediti pravila zaključivanja tako da sadrže ꓕ.

Sada pravila izgledaju ovako (na primeru zbira, pokrili smo greške)

S⊢e1:T1
S⊢e2:T2
T 1 ≤ double
T 2 ≤ double
---------------
S ⊢ e 1 + e 2 : double

Oporavljanje od grešaka:
• Neophodno je da postoji neka vrsta oporavka od greške u semantičkom analizatoru
• Jedan tip oporavka je tip error koji smo uveli, ali postoje i drugi tipovi i slučajevi koje treba
rešiti kao što su poziv nepostojeće funkcije, deklaracija neispravnog tipa i slično.

Ne postoje ispravna i pogrešna rešenja, već samo lošiji ili bolji izbori.
26. Preopterećivanje funkcija.

Više funkcija sa istim imenom a različitim argumentima.


U fazi kompilacije, analizom tipova argumenata, potrebno je odrediti koju funkciju treba pozvati.
U slučaju da nije moguće odrediti, treba prijaviti grešku.

Primer.
int Function();
int Function(int x);
int Function(double x);
int Function(Base b);
int Function(Derived d);

Jednostavno preopterećivanje:
• Počinjemo od skupa preopterećenih funkcija
• Najpre isfiltriramo funkcije koje se ne poklapaju i tako dobijamo skup kandidata.
• Ako je skup prazan prijavimo grešku
• Ako skup sadrži samo jednu funkciju, biramo nju
• Ako sadrži više biramo najbolju.

Kako odrediti najbolje poklapanje?


• Ako postoji više od jednog izbora, izaberemo onaj koji najspecifičnije odgovara pozivu
• Ako imamo dve funkcije koje su kandidati, A i B sa argumentima a1...an i b1...bn. Kažemo
da je A <: B ako važi Ai < Bi, za svako i e (0, n+1).
• Funkcija A je najbolji izbor ako za svaku drugu funkciju B važi A <: B (tj. da je ona barem
onoliko dobra koliko i svaki drugi izbor)
◦ Ako postoji takav izbor onda je to najbolje poklapanje.
◦ U suprotnom poziv je višesmislen i mora nekako da se razreši.

Preopterećivanje u realnosti:
• Preopterećivanje je obično značajno kompleksnije
• Na primer neki jezici (npr C, C++, Java) dozvoljavaju funkcije sa promenljivim brojem
argumenata. (variadic functions)

Preopterećivanje u kontekstu variadic funkcija:


• Smatraj poziv višesmislenim
• Smatraj boljom funkciju koja nije variadic.
◦ Funkcija koja je specifično dizajnirana da podrži neki konkretan skup argumenata je
verovatno bolji izbor id variadic funkcije koja pokriva taj skup elemenata.
◦ Ova opcija se koristi u C++ i sa malim modifikacijama u Javi

Hijerarhijsko preopterećivanje funkcija:


• Napraviti hijerarhiju funkcija kandidata
• Konceptualno to je vrlo slično dosezima
◦ Počni na najnižem nivou i traži u okviru njega poklapanje
◦ Ako pronađeš jedinstveno poklapanje izaberi ga
◦ Ako pronađeš višestruko poklapanje prijavi višesmislenost
◦ Ako ne pronađeš poklapanje idi na naredni nivo u hijerarhiji
27. Kompletnost i saglasnost sistema tipova. Kovarijanta povratnog tipa. Kovarijanta po argumentu
funkcije. Kotravarijanta po argumentu funkcije.

A Rule for Member Functions

f is an identifier.
S⊢e0:M
f is a member function in class M.
f has type (T 1 , ..., T n ) → U
S ⊢ e i : R i for 1 ≤ i ≤ n
R i ≤ T i for 1 ≤ i ≤ n
-----------------------------
S ⊢ e 0 .f(e 1 , ..., e n ) : U

Statički i dinamički tipovi:


• Statički tipovi podataka su često nepotpuni. To se dešava zato što postoji razlika u izvornom
kodu:
◦ Statički tip je deklarisan u izvornom kodu
◦ Dinamički tip je pravi tip objekta u fazi izvršavanja

(i) Kompletnost (potpunost) i saglasnost:


Idealan sistem: kompletan (potpun) i saglasan:

Kompletnost: Ako se program može ispravno izvršiti -onda važi da sistem tipova kaže da je on
ispravan u smislu tipova.

Saglasnost: Ako sistem tipova kaže da je ispravan - onda važi da se on može ispravno izvršiti.

Nažalost za većinu programskih jezika, nemoguće je ostvariti i potpunost i saglasnost.

Saglasnost se može obezbediti dokazivanjem narednog svojstva za svaki izraz E:


DynamicT ype(E) ≤ StaticT ype(E)
Jer kao što je napomenuto statički sistemi tipova mogu nekad da odbiju program koji bi se mogao
izvršiti (zato što nisu u mogućnosti da dokažu odsustvo greške u tipovima) Takav sistem tipova
naziva se nekompletan.

(ii) Kovarijanta po povratnom tipu

Neka funkcija A predefiniše funkciju B, ali neka se ove funkcije razlikuju po povratnom tipu:
funkcije A i B su kovarijante po povratnom tipu ukoliko se tipovi argumenata poklapaju, a povratni
tip funkcije A se može konvertovati u povratni tip funkcije B.

f is an identifier.
S⊢e0:M
f is a member function in class M.
f has type (T 1 , ..., T n ) → U
S ⊢ e i : R i for 1 ≤ i ≤ n
R i ≤ T i for 1 ≤ i ≤ n
----------------------------
S ⊢ e 0 .f(e 1 , ..., e n ) : U
Kovarijante povratnog tipa su bezbedne, tj. pravilo koje definiše tipove je i dalje saglasno jer je sa
kovarijantom povratnog tipa obezbeđeno važenje uslova DynamicT ype(E) ≤ StaticT ype(E).

Mnogi programski jezici omogućuju kovarijante povratnog tipa. na primer C++ i Java

(iii) Kovarijanta po argumentu

Neka funkcija A predefiniše funkciju B, ali neka se ove funkcije razlikuju po bar jednom
argumentu. Funkcija A je kovarijanta po argumentu funkcije B ukoliko se argumenti funkcija
poklapaju ili se neki argumenti funkcije A mogu pretvoriti u odgovarajuće argumente funkcije B.

f is an identifier.
S⊢e0:M
f is a member function in class M.
f has type (T 1 , ..., T n ) → U
S ⊢ e i : R i for 1 ≤ i ≤ n
R i ≤ T i for 1 ≤ i ≤ n
----------------------------------
S ⊢ e 0 .f(e 1 , ..., e n ) : U

*Kovarijante po argumentu su nebezbedne!!

U nekim jezicima ovo nije dobro urađeno: na primer, Eiffel dozvoljava


kovarijante po argumentima i posledica toga je da na taj način neispravni
programi mogu da promaknu sistemu tipova i da zato dode do greške u
fazi izvršavanja.

Primer zašto nije bezbedno:


class Fine {
void nothingFancy(Fine f) {
/* ... do nothing ... */
}
}

class Borken extends Fine {


int missingFn() {
return 137;
}

void nothingFancy(Borken b) {
Print(b.missingFn());
}
}

int main() {
Fine f = new Borken;
f.nothingFancy(new Fine);
}
(iii) Kontravarijanta po argumentu funkcija

Neka važi C a ≤ C b ≤ C c , tj neka je C c bazna klasa za klasu C b , a C b


bazna klasa za klasu C a . Neka funkcija A (klase C a ) predefiniše funkciju
B (klase C b ), ali neka se razlikuju tipovi arugmenata, tj: funkcija A je
kontravarijanta po argumentu funkciji B ukoliko A ima za argument tip
superklase C c umesto tip C b .

Da li je bezbedno dozvoliti kontravarijante po argumentima?


Jeste. intuitivno, kada zovemo tu funkciju kroz baznu klasu, funkcija će prihvatiti bilo šta što bi
bazna klasa već prihvatila.

28. Izvršno okruženje i podaci. Enkodiranje osnovnih tipova, nizova i višedimenzionih nizova.

Prikaz podataka:
• Mašine obično podržavaju samo ograničen skup tipova
◦ Celobrojne vrednosti fiksirane dužine
◦ Brojeve u pokretnom zarezu

Enkodiranje osnovnih tipova:


• Osnovni celobrojni tipovi, kao što su byte, char, short, int, long, se preslikavaju direktno u
odgovarajuće mašinske tipove
• Osnovni realni tipovi se takođe direktno preslikavaju u mašinske tipove
• Pokazivači se obično implementiraju kao celobrojni tip koji čuva memorijske adrese

Nizovi:
• C style. Elementi su jedan za drugim u memoriji
• Java style. Elementi su jedan za drugim, stim što je prvi element int broj elemenata niza
• D style. Elementi su jedan za drugim, stoga što svaki element čuva pokazivač na prvi i član
posle poslednjeg člana.

Višedimenzioni:
• Predstavljaju se kao nizovi nizova
• Oblik zavisi od nizova koji se koriste.

Funkcije
Potrebno je odgovoriti na veliki broj pitanja:
• Kako izgleda dinamičko izvršavanje funkcija?
• Gde se izvršivi kod za funkcije nalazi?
• Kako se prosleđuju parametri funkcije?
• Gde se čuvaju lokalne promenljive?

Odgovori zavise od željenih karakteristika jezika.

29. Izvršno okruženje i funkcije. Aktivaciono stablo. Zatvorenja i korutine. Stek izvršavanja.

Aktivaciono stablo

• Aktivaciono stablo je stablo struktura koje predstavljaju sve pozive funkcija prilikom nekog
konkretnog izvršavanja programa.
• Aktivaciono stablo se ne može uvek odlučiti u fazi kompilacije.
• Statički ekvivalent je graf poziva funkcija
• Svaki aktivacioni slog čuva kontrolni link prema aktivacionom slogu koji ga je inicirao.
• Aktivaciono stablo je špageti stek.

Zašto možemo uvek da optimizujemo špageti stek? Pretpostavke:


• Jednom kad se funkcija vrati, njen aktivacioni slog ne može da bude ponovo referenciran.
◦ Prema tome nema potrebe da čuvamo stare čvorove u aktivacionom stablu.
• Svaki aktivacioni slog je ili završio izvršavanje ili je predak tekućeg aktivacionog sloga.
Nema potrebe da čuvamo više grana izvršavanja u jednom trenutku.

Zatvorenja

*Jednom kad se funkcija vrati, njen aktivacioni slog ne može da bude ponovo referenciran.

Ova pretpostavka ne važi za zavorenja (closures)

Zatvorenje Z je unježdena funkcija u funkciju F koja ima mogućnost pristupa slobodnim


promenljivama iz funkcije F, pri čemu je funkcija F završila sa svojim izvršavanjem. Osnovne
karakteristike zatvorenja:
• to je ugnježdena funkcija
• ona ima pristup slobodnim promenljivama iz spoljašnjeg dosega
• ona je vraćena kao povratna vrednost funkcije u koju je ugnježdena

Primer zatvorenja

function CreateCounter() {
var counter = 0;
return function() {
counter ++;
return counter;
}
}

Kontrolni link funkcije je pokazivač na funkciju koja ju je pozvala, koristi se da se odredi gde se
izvršavanje nastavlja kada funkcija završi sa radom.

Pristupni link funkcije je pokazivač prema aktivacionom slogu u kojem je funkcija kreirana.
Koriste ga ugnježdene funkcije da odrede lokaciju promenljivih kojima mogu da pristupe.

Korutine

*Svaki aktivacioni slog je ili završio izvršavanje ili je predak tekućeg aktivacionog sloga.

Podsetimo se:
• Podrutine su funkcije koje, kada se pozovu, izvršavaju se do završetka svog posla i vraćaju
kontrolu funkciji pozivaocu.
• Korutine su funkcije koje kada se pozovu, urade deo posla, vrate kontrolu pozivaocu, ali
kasnije mogu da nastave od mesta gde su stale.

Podrutine su specijalan slučaj korutina.


Stek izvršavanja

• Logički izgled stek okvira obično kreira IR generator. Logički izgled


obično ignoriše detalje o mašinski specifičnim konvencijama poziva funk-
cija
• Fizički izgled stek okvira kreira generator koda. On je zasnovan na
logičkom izgledu koji postavlja IR generator i uključuje frame pointere,
caller-saved registre i slične detalje

30. Izvršno okruženje i objekti. Strukture, objekti i nasledivanje.

Implementacija objekata je veoma složena. Veoma je teško napraviti izražajan i efikasan objektno
orijentisan jezik.

Koncepti koje je teško implementirati:


• Dinamičko raspoređivanje (virtualne funkcije)
• Interfejsi i višestruko nasleđivanje
• Dinamičku proveru tipova (instanceof)

Strukture, objekti i nasleđivanje.

Strukture:
Struktura je tip koja sadrži kolekciju imenovanih vrednosti.
Najčešći pristup, postaviti svako polje redom u memoriji onim redom kojim je deklarisano.

Pristup poljima strukture:


• Jednom kada se objekat postavi u memoriju, on je samo serija bajtova
• Potrebno je znati gde tražiti konkretno polje.
◦ Ideja: Čuvati internu tabelu u okviru kompajlera koja sadrži pomeraje za svako polje
◦ Da bi se našlo traženo polje, počni od osnovne adrese i pomeri je unapred za
odgovarajući pomeraj (offset)

Primer:
MyStruct* ms = new MyStruct;
ms->x = 137; store 137 0 bytes after ms
ms->y = 'A'; store 'A' 4 bytes after ms
ms->z = 2.71 store 2.71 8 bytes after ms

Jednostruko nasleđivanje
• Pojednostavljeno zbog memorije za izvedenu klasu D je zadat kao izgled memorije za baznu
klasu B za kojom sledi izgled memorije za preostale članove klase D
• Pokazivač na D i dalje vidi objekat B na početku memorije
31. Izvršno okruženje i funkcije članice klasa. Pokazivač this i dinamičko odredivanje poziva.

Funkcije članice klasa su kao i obične funkcije samo sadrže dve komplikacije:
• Kako znati za koji objekat je vezana funkcija?
• Kako znati koju funkciju pozvati u fazi izvršavanja?

U okviru funkcije klase ime this se koristi za tekući objekat. Ova informacija treba da se
iskomunicira funkciji.

Ideja: Tretiraj this kao implicitni parametar funkcije. Svaka funkcija koja ima n-argumenata ce u
stvarnosti imati n+1 argument zbog ovoga.

Kad se generiše kod, za poziv funkcije članice prosleđuje se još jedan parametar this koji
predstavlja odgovarajuči objekat.
U okviru funkcije članice this je samo još jedan parametar funkcije.

Kada se implicitno referiše na neko polje od this, koristi se ovaj dodatni


parametar kao objekat u kojem treba da se traži odgovarajuće polje

Dinamičko određivanje poziva znači odabir poziva funkcije u fazi izvršavanja u zavisnosti od
tekućeg dinamičkog tipa objekta.
Ideja: U fazi kompilacije napravi listu svih klasa i generiši naredni kod:

if (the object has type A)


call A’s version of the function
else if (the object has type B)
call B’s version of the function
...
else if (the object has type N)
call N’s version of the function.

Ideja ima mane:


• Veoma je spora, složenost je O(C), gde je C broj klasa
• Nije uvek ostvarivo rešenje, recimo ako imamo više izvornih fajlova.

Rešenje je tabela virtuelnih funkcija i tabela metoda.


32. Tabela virtulenih funkcija i tabela metoda. Višestruko nasledivanje i interfejsi.

Tabela virtuelnih funkcija vtable je niz pokazivača na implementacije funkcija članica neke klase.
Da bi se pozvala funkcija članica klase:
• Odredi statički indeks u virtuelnoj tabeli
• Prati pokazivač koji se nalazi na tom indeksu u okviru vtable objekta da bi došao do koda
funkcije
• Pozovi tu funkciju

Analiza ovog pristupa:


• Prednosti: vreme da se odredi funkcija koja će biti pozvana O(1)
• Mane: Objekti su veći jer svaki objekat mora da ima prostor da skladišti O(M) pokazivača,
gde je M broj članica klase. Zbog toga je i kreiranje malo sporije.

Dinamička rešavanja u O(1):


• Kreira se jedinstvena instanca vtable za svaku klasu
• Svaki objekat čuva pokazivač na svoj vtable
• Svaki objekat može da prati taj pokazivač u O(1) i može da prati index u tabeli u vremenu
O(1)
• Povećava se veličina objekta za O(1)
• Ovo rešenje se koristi u većini C++ i Java implementacija

Zahtevi:
• Za prethodno rešenje napravljene su implicitne pretpostavke o jeziku koje dozvoljavaju
vtable radi korektno:
◦ Svi metodi su poznati statički: Možemo u fazi kompilacije da utvrdimo koji metod je
planiran na mestu poziva
◦ Jednostruko nasleđivanje: ne moramo da brinemo o građenju jedinstvene tabele vtable
prilikom višestrukog nasleđivanja

*Neki programski jezici ne mogu da rade sa virtuelnim tabelama, recimo php.

Opšti okvir nasleđivanja:


• Svaki objekat čuva pokazivač na deskriptor svoje klase:
◦ Svaki deskriptor klase čuva:
▪ Pokazivač na tabelu metoda
▪ Pokazivač na baznu klasu
• Ukoliko se ne nađe u tabeli metoda funkcija, onda se traži u tabeli metoda bazne klase

(ii) Višestruko nasleđivanje i interfejsi

Višestruko nasleđivanje i interfejsi komlikuju izlged virtuelne tabele jer zahtevaju da metodi imaju
konzistentne pozicije kroz sve virtuelne tabele: može da se desi da virtuelna tabela ima
neiskorišćene unose (prazna mesta)

Zbog toga, prilikom višestrukog nasleđivanja i interfejsa obično se ne koriste virtuelne tabele.

Postoje različiti načini da se ovo implementira, jedan način je hibridni pristup: korišćenje virtuelnih
tabela za standradno nasleđivanje, a za interfejse koristi metod za poređenje metod zasnovan na
poređenju stringova.
33. Implementiranje dinamičkih provera tipova.

Mnogi jezici imaju potrebu za dinamičkim proverama tipova, Java instanceof, C++ dynamic_cast ,
jezici koji su dinamički tipizirani.

Ono što možemo da želimo da odredimo je da li je dinamički tip pretvoriv u neki drugi tip (<=)
Kako to implementirati?

Rešenje:
• Implementirati da svaka vtable čuva i pokazivač na svoju osnovnu klasu
• Provera da li se objekat može pretvoriti u neki tip S u fazi izvršavanja se svodi na praćenje
pokazivača na roditelja u okviru virtuelne tabele sve dok ne naiđe na tip S ili dok ne dođe do
tipa koji nema roditelja.
• Složenost je O(d) gde je d dubina hijerarhije.

Da li se ovo može odraditi brže?

Ideja je zasnovana na činjenici: objekat koji je statički tipa A je u fazi izvršavanja tipa B samo ako
je A pretvorivo u B, tj. A <= B.

• Ideja: Dodeli svakoj klasi jedinstveni prost broj. (kroz različite hijerarhije se mogu koristiti
različiti prosti brojevi)
• Postavi ključ klase da bude proizvod njenog prostog broja i svih prostih brojeva njenih
nadklasa
• Da bi se proverilo u fazi izvršavanja da li se objekat može pretvoriti u tip T:
◦ Pogledaj ključ objekta
◦ Ako je ključ T deljiv sa ključem objekta, onda se objekat može kovertovati
◦ Ako nije deljiv onda ne može.
• Metod se može koristiti i kod višestrukog nasleđivanja

34. Troadresni kod. Aritmetičke i bulovske operacije. Kontrola toka.

Međureprezentacija visokog nivoa, u kojoj svaka operacija ima tri operanda.


U trenutku generisanja troadresnog koda nije bitno misliti na njegovu implementaciju.
U redu je da taj kod sadrži dodele viška i redundantna izračunavanja - to će sve biti eliminisano u
fazi optimizacije.

Evaluacija izraza sa više od tri operanda zahteva pomoćne promenljive.

(ii) Aritmetičke i bulovske operacije

Bulovske vrednosti se predstavljaju kao celobrojne vrednosti koje mogu imati vrednost nula ili
vrednost različitu od nule. Treba malo ovde pricati o if, while i for i kako se rešavaju preko goto.

Kontrola toka, labele:


• Imenovane labele označavaju delove koda na koje se može skočiti
• goto label - bezuslovni skok
• ifZ value goto label; - uslovni skok
35. Troadresni kod. Funkcije i stek okviri.

Priča iz prethodnog pitanja za troadresni kod.

Funkcije se sastoje iz četiri dela:


• Labela koja označava početak funkcije
• Instrukcija beginFunc N; koja rezerviše N bajtova za prostor lokalne i privremene
promenljive.
• Telo funkcije
• EndFunc koje obeležava kraj funkcije, ona je zadužena da počisti stek okvir i vrati kontrolu
na odgovarajuće mesto.

Upravljanje stekom u troadresnom kodu.

Funkcija pozivaoc je odgovorna za stavljanje arugmenata funkcije na stek.


Funkcija koja je pozvana je odgovorna za smeštanje svojih lokalnih i privremenih promenljivih.

• Instrukcija BeginFunc N; rezerviše prostor za lokalne i privremene pro-


menljive
• Instrukcija EndFunc; vraća prostor koji je rezervisan sa BeginFunc N;
• Jedna parametar je gurnut na stek od strane pozivaoca korišćenjem in-
strukcije PushParam var
• Prostor je osloboden od strane pozivaoca korišćenjem instrukcije PopParams
N;
• N se meri u bajtovima, ne po broju argumenata!

Primer koda:

void main() {
SimpleFunction(137);
}

Troadresna verzija:

main:
BeginFunc 4;
_t0 = 137;
PushParam _t0;
LCall _SimpleFn;
PopParams 4;
endFunc;

• Interno, procesor ima specijalni registar koji se naziva brojač instrukcija


program counter (PC) koji čuva adresu naredne instrukcije koja treba da
se izvrši
• Kad kôd funkcije završi sa radom, potrebno je da se PC podesi tako da se
nastavi izvršavanje funkcije tamo gde je ono bilo prekinuto
• Parametri počinju od adrese frame pointer +4 i rastu naviše.
• Lokalne promenljive i privremene promenljive počinju od adrese fp-8, i
rastu naniže

Global pointer: MIPS takođe ima registar koji se naziva global pointer i koji čuva adresu globalnog
prostora.
Memorija na koju pokazuje globalni pointer se tretira kao niz vrednosti koji rastu naviše
Potrebno je odrediti za svaku globalnu promenljivu pomeraj u ovom nizu tj gde se ona tačno nalazi:
Global variable N --> adresa: global pointer + 4 * N

Rezime:
• Većina detalja je apstrakovana IR formatom
• Parametri počinju na adresi fp + 4 i rastu naviše
• Lokalne adrese počinju na adresi fp - 8 i rastu naniže
• Globalne promenljive počinju od adrese gp + 0 i rastu naviše

36. Troadresni kod za objekte. Dinamičko razrešavanje poziva.

Pristup memoriji:
• Potrebno je da proširimo troadresni kod tako da ima mogućnost pristupa memoriji, tj.
potrebno je da pristup poljima objekta pretvorimo u relativne memorijske adrese

Dinamičko razrešavanje poziva.


• Adresa virtuelne tabele objekta može da se referencira kroz ime dodeljeno virtuelnoj tabeli,
obično je to ime isto kao ime objekta npt _to = Base;
• Kada se kreira objekat, mora prvo da se postavi pokazivač na virtuelnu tabelu
• Instrukcija ACall može da se koristi za poziv metoda kada je za njega dat pokazivač na prvu
instrukciju

37. Algoritam generisanja troadresnog koda.

U ovom stadijumu kompilacije imamo na raspolaganju:


• AST
◦ koji je anotiran sa informacijama o dosegu
◦ koji je anotiran sa informacijama o tipovima

Da bi se generisao TAC (Troadresni kod) potrebno je još jednom obići rekurzivno AST:
• Generiši TAC izraz za svaki podizraz i podnaredbu
• Koristeći generisani TAC za podizraze/naredbe, generiši TAC za kompletne izraze i naredbe

Generisanje TAC-a za izraze:


• Definiši funkciju codegen (expr) koja generiše TAC koji računa izraz, čuva njenu vrednost u
privremenoj promenljivoj i vraća ime te promenljive
• Definiši codegen(expr) direktno za atomičke izraze (konstante , this, identifikatore i slično)
• Definiši codegen(expr) rekurzivno za složene izraze (binarne operatore, pozive funkcija)

Generisanje naredbi:
• Možemo proširiti funkciju codegen tako da radi sa naredbama
• Za razliku od codegen za izraze, codegen za naredbe ne vraća ime promenljive koja čuva
vrednost
38. Optimizacije medukoda. Graf kontrole toka.

• Cilj optimizacije je da se poboljša kod generisan u prethodnom koraku


• To je najvažniji i nakompleksniji deo kompilatora
• Veoma aktivna oblast istraživanja
• Razlozi za optimizaciju:
◦ Prilikom generisanjam međukoda uvodi se redundantnost
◦ Programeri često ne razmišljaju o efikasnosti koda (lenji smo)

Termin optimizacija označava traženje optimalnog koda za dati program


Ovaj problem je u opštem slučaju neodlučiv

Karakteristike dobrog optimizatora:


• Ne sme suštinski da menja ponašanje programa
• Treba da proizvede što efikasniji kod
• Ne treba da koristi puno vremena
Nažalost:
• I dobri optimizatori naprave grešku nekad
• Često promaše da urade neku jednostavnu optimizaciju zbog ograničenja algoritama koje
koriste
• Većina interesantnih optimizacija je NP-teška ili neodlučiva

Šta optimizujemo:
• Vreme izvršavanja
• Upotreba memorije
• Upotreba energije (biramo jednostavne instrukcije)

Optimizacija koda vs optimizacija međukoda


• Obično optimizacija međukoda sprovodi pojednostavljivanja koja su validna za sve
arhitekture dok optimizacija koda pokušava da unapredi performanse na osnovu
karakteristika ciljne arhitekture računara.
• Neke optimizacije su negde izmedu, na primer zamena x*0.5 sa x/2

Osnovni blok:
• Niz IR instrukcija gde:
◦ Postoji jedna tačka ulaska u blok i nalazi se na vrhu bloka.
◦ Postoji jedna tačka izlaska iz bloka i nalazi se na kraju bloka.

• Neformalno niz instrukcija u bloku se uvek izvršava u celini.

(ii) Graf kontrole toka


• Graf kontrole toka je graf koji sadži osnovne blokove funkcije.
• Svaka ivica iz jednog osnovnog bloka do drugog označava da kontrola toka izvršavanja
može da ide od kraja prvog do početka drugog bloka
• Postoje i dva čvora koja označavaju početak i kraj funkcije
39. Lokalne optimizacije medukoda. Eliminacija zajedničkih podizraza. Prenos kopiranja.
Eliminacija mrtvog koda.

Lokalna optimizacija radi u okviru jednog osnovnog bloka.

(i) Eliminacija zajedničkih podizraza.

Ako imamo dve dodele

v1 = a op b
v2 = a op b

tako da u vrednosti promenljivih a i b ne menjaju između između ove dve dodele, onda možemo da
prepišemo kod na sledeći način.

v1 = a op b
v2 = v1

Na ovaj način se eliminiše bespotrebno izračunavanje i pravi se prostor za kasnije optimizacije.

(ii) Prenos kopiranja

Ako imamo dodelu

v1 = v2

onda sve dok v1 i v2 nemaju ponovo dodeljene neke nove vrednosti možemo da zapišemo izraze
oblika

a = ... v1 ...
kao izraze oblike
a = ... v2 ...

pod uslovom da je takvo prezapisivanje u redu.

Ova optimizacija stvara prostor za naredne optimizacije koje ćemo uskoro videti.

(iii) Eliminacija mrtvog koda

• Dodela promenljivoj v se naziva mrtva dodela ako se vrednost v nigde kasnije ne koristi.
• Eliminacija mrtvog koda uklanja mrtve dodele iz međureprezentacije
• Utvrđivanje da li je dodela mrtva zavisi od toga kojim promenljivama se dodeljuju vrednosti
i gde se ta dodela vrši.

Različite optimizacije koje smo do sada videli sve zapravov vode računa o samo malom delu
optimizacije.
• Optimizacija zajedničkih podizraza eliminiše nepotrebne naredbe.
• Prenos kopiranja pomaže u pronalaženju mrtvog koda
• Eliminacija mrtvog koda uklanja naredbe koje više nisu potrebne
• Da bi se izvukao maksimum ove operacije možda moraju da se izvrše više puta
40. Implementacija lokalnih optimizacija. Analiza dostupnih izraza. Analiza živosti.

(i)Analiza dostupnih izraza


• I eliminacija zajedničkih podizraza i prenos kopiranja zavise od analize dostupnih izraza u
programu
• Izraz se naziva dostupan ako neka promenljiva u programu čuva vrednost tog izraza
• U okviru eliminacije zajedničkih izraza, zamenjujemo dostupni izraz sa promenljivom koja
nosi tu vrednost.
• U prenosu kopiranja menjamo korišćenje promenljive sa dostupnim izrazom koja ona
koristi.

Pronalaženje dostupnih izraza:


• Inicijalno, ni jedan izraz nije dostupan
• Kada god se izvrši izraz a = b + c
◦ Svaki izraz koji sadrži promenljivu a više nije validan
◦ Izraz a = b + c postaje dostupan
• Ideja: iteriraj kroz instrukcije osnovnog bloka počevši sa praznim skupom izraza i ažuriraj
dostupne izraze nakon svake instrukcije.

(ii) Analiza živosti

Analiza koja odgovara eliminaciji mrvog koda se naziva analiza živosti.


Promenljiva je živa u tački programa ako se kasnije u tom programu njena vrednost čita pre nego
što se u nju nešto upiše.
Eliminacija mrtvog koda radi tako što sračunava živost svake promenljive i onda eliminiše dodele
mrtvim promenljivima.

Izračunavanje živih promenljivih


• Da bismo znali koja promenljiva će biti korišćena u nekoj tački programa iteriramo kroz sve
naredbe osnovnog bloka u obrnutom redosledu
• Inciijalno, neki mali skup vrednosti se zna da će biti živ: koji tačno, zavisi od konkretnog
programa.
• Kada vidimo naredbu oblika a = b +c
◦ Neposredno pre te naredbe, a nije živa, jer će njena vrednost da bude prepisana
◦ Neposredno pre te naredbe, b i c su žive jer će obe biti korišćene
• Primetimo da nam međukod često ne dozvoljava naredbu a = a + b. U slučaju da je takva
naredba dozvoljena, onda je živost jača odnosno, najpre se vrši uklanjanje promenljivih iz
skupa, pa onda dodavanje, tako da će a nakon ove analize ipak biti živa promenljiva.

Informacije potrebne za lokalnu analizu:


• U kojem smeru se vrši analiza? Unapred ili unazad?
• Na koji način ažuriramo informacije prilikom obrade pojedinačne naredbe?
• Koje informacije imamo inicijalno?
41. Lokalna analiza formalno.

Lokalna analiza osnovnog bloka se definiše kao uređena četvorka (D, V, F, I) gde je:
• D direction, odnosno smer. Može biti unapred ili unazad.
• V je skup vrednosti koje program ima u svakoj tački
• F je familija funkcija prenosa, koje definišu značenje svakog izraza kao funkciju f: V -> V
• I initial. To je početna informacija na vrhu ili dnu osnovnonog bloka

Analiza dostupnih izraza:


• Smer: Unapred
• Domen: Skup izraza dodeljenih promenljivama
• Funkcije prenosa: Za dati skup dodela V i naredbu a = b + c
◦ Ukloni iz V svaki izraz koji koristi a kao podizraz
◦ Dodaj u V izraz a = b + c
• Početne vrednosti: Prazan skup izraza

Analiza živosti:
• Smer: Unazad
• Domen: Skup promenljivih
• Funkcije prenosa: Za dati skup promenljivih i naredbu a = b + c:
◦ Ukloni a iz V (svaka prethodna vrednost od a je sada mrtva)
◦ Dodaj u V promenljive b i c (svaka prethodna vrednost od b i c je sada živa)
• Početne vrednosti: Zavisi od semantike jezika

42. Globalne optimizacije. Glavni izazovi.

• Globalna analiza je analiza koja radi na grafu kontrole toka


• Suštinski je moćnija od lokalne analize ali i suštinski komplikovanija od lokalne analize
• Mnoge optimizacije zasnovane na lokalnoj analizi mogu da se sprovedu i globalno
• Neke optimizacije ne mogu da se sprovedu lokalno već mogu samo globalno
• Primeri globalnih optimizacija:
◦ Globalna eliminacija mrtvog koda
◦ Globalno kopiranje konstanti
◦ Parcijalna eliminacija redudantnosti

Glavni izazovi:
• U okviru globalne analize, svaka naredba može imati više prethodnika.
• Globalna analiza mora imati neku vrstu kombinovanja informacija od svih prethodnika
osnovnog bloka
• U okviru globalne analize, može postojati puno puteva kroz kontrolni graf. (CFG)
• Može da bude potrebno da se ponovo izračunaju vrednosti više puta, tj. uvek kada nove
informacije postanu dostupne.
• Treba biti oprezan kako se ne bi desilo da se upadne u beskonačnu petlju.
• U okviru globalne analize sa petljama, svaki blok može da zavisi od svakog drugog bloka
◦ Da bismo ovo rešili, moramo da dodamo nekakve početne vrednosti svim blokovima.

Sumiranje razlika:
• Potrebno je da možemo da obradimo više prethodnika/sledbenika osnovnog bloka
• Potrebno je da možemo da obradimo više putanja kroz CFG i može biti potrebe da se iterira
više puta da bi se sračunala konačna vrednost.
• Potrebno je da možemo da dodelimo svakom osnovnom bloku smislenu osnovnu početnu
vrednost pre nego što ga analiziramo.
43. Globalna analiza živosti.

• Na početku postavu IN(s) = prazan skup za svaku naredbu s


• Postavi IN(exit) na skup promenljivih za koje se zna da su žive na izlazu.
• Ponavljaj sve dok ima promena:
◦ Za svaku naredbu s oblika a = b + c u bilo kom redosledu obilaska
▪ Postavi OUT[s] na skup unije od IN[p] za svaki sledbenik p od naredbe s
▪ Postavi IN[s] na (OUT[s] - a) U {b, c}

Da li je ovakav algoritam dobar?


• Da bi pokazali korektnost, potrebno je da pokažemo naredno:
◦ Algoritam se zaustavlja
◦ Kada se algoritam zaustavi, daje saglasno rešenje
• Argumenti za zasustavljanje:
◦ Kada se pronađe da je neka promenljiva živa u nekom delu programa, to uvek važi
◦ Postoji konačno mnogo promenljivih i konačno mnogo mesta na kojima promenjive
mogu da postanu žive
• Argumenti o saglasnosti:
◦ Svako pojedinačno pravilo, primenjeno na neki skup, korektno ažurira živost skupa
◦ Kada se računa unija dva skupa živih promenljivih, promenljiva je živa samo ako je bila
ćiva na nekoj putanji koja vodi do naredbe

44. Polumreže sa operatorom spajanja.

• Polumreža sa operatorom spajanja je uređenje definisano na skupu elemenata


• Svaka dva elementa imaju spajanje koje je najveći element koji je manji od oba elementa
• Intuitivno, spajanje dva elementa predstavlja kombinovanje informacija od ta dva elementa i
to nekakva uopštenija informacija od informacija koje su bile početne.
• Postoji jedinstven element na vrhu, koji se naziva i top, koji je veći od svih drugih elemenata
• Element na vrhu predstavlja "nema informacija" (veoma precizna informacija)

Formalna definicija:
• Polumreža sa operatorom spajanja je uređeni par (D, ∧) gde je
◦ D je skup koji označava domen elemenata
◦ ∧ je operator spajanja koji je
▪ idempotentan x ∧ x = x
▪ komutativan x ∧ y = y ∧ x
▪ asocijativan (x ∧ y) ∧ z = x ∧ (y ∧ z)
◦ Ako važi i x ∧ y = z , kažemo da je z spajanje za x i y
• Svaka polumreža sa operatorom spajanja ima element na vrhu koji se označava sa T takav
da je T ∧ x = x za svako x.

Primer - polumreža za analizu živosti


• Skup živih promenljivih i operacija unije
• Idempotencija a U a = a
• Komutativnost važi takođe
• Asocijativnost takođe
• Top element je prazan skup.
45. Algoritmi globalne analize medukoda.

Polumreže i analiza algoritama.


• Polumreže prirodno rešavaju veliki broj problema na koje nailazimo u globalnoj analizi
• Kako kombinujemo informacije iz različitih osnovnih blokova? - Koristimo operator
spajanja
• Koju vrednost dajemo kao početnu vrednost vakom bloku? - Vrednost top
• Kako znamo da će se program završiti? - Zapravo to ne možemo još uvek da garantujemo,
potrebno je da postoje određene osobine polumreže kao i funkcije spajanja.

Opšti okvir

• Globalna analiza je petorka (D, V, ∧, F, I) pri čemu je


• D smer - napred, nazad
• V je skup vrednosti
• ∧ je operator spajanja na ovim vrednostima
• F je skup funkcija prenosa f : V → V – I je početno stanje

Jedina razlika u odnosu na lokalnu analizu je uvodenje operatora spajanja.

Algoritam globalne analize:


• Pretpostavimo da je (D, V, ∧, F, I) analiza unapred
• Postavi OUT[s] = T za svaku naredbu s
• Postavi OUT[begin] = I
• Ponavljaj dok ima izmena:
◦ Za svaku naredbu s sa prethodnicima p1..pn
▪ Postavi IN[s] = OUT[p1]∧...∧ OUT[pn]
▪ Postavi OUT[s] = f(IN[s])
◦ Redosled iteracija nije važan

Ova vrsta analize naziva se okvir toka podataka. Uz dodatne pretpostavke može se koristiti da se
dokaže svojstvo završavanja analize.

46. Generisanje koda. Izazovi alokacije registara. Naivni algoritam.

U ovom trenutku imamo optimizovan IR kod koji treba da se konvertuje u odgovarajući jezik ciljne
mašine.

Ciljevi:
• Izabrati odgovarajuće mašinske instrukcije za svaku IR instrukciju
• Podeliti resurse mađine (registri, keš)
• Implementirati detalje niskog nivoa izvršnog okruženja

Mašinski specifične optimizacije se često rade u ovom delu kompilatora

Memorija:
• Postoji velika razlika u brzini i veličini memorije.
◦ RAM je brza ali skupa, hard disk je jeftin, ali veoma spor.
◦ Osnovna ideja je dobiti najbolje iz svih svetova korišćenjem različitih vrsta memorije
Izazovi generisanja koda:
• Skoro svi jezici imaju veoma grub pogled na memorijsku hijerarhiju: sve promenljive žive u
"memoriji" bez zalaženja u detalje na koju se tačno memoriju misli. Jedino se sa diskom i
mrežom eksplicitno rukuje drugačije.
• Izazov generisanja koda je da se objekti postave na takav način koji maksimizuje prednosti
memorijske hijerarhije.
◦ Dodatno, to je potrebno uraditi potpuno automatski, tj bez pomoći programera.

Izazovi alokacije registara:


• Mali broj registara
◦ Obično neuporedivo manji od broja promenljivih u IR-u
◦ Potrebno je naći način da se registri ponovo iskoriste uvek kada je to moguće
• Registri su komplikovani

Naivni algoritam za alokaciju registara

Sekvencijalno se popunjavaju registri.

Mane:
• Ogroman broj učitavanja
• Jako je neefikasan
• Veoma lako postaje za red veličine ili dva reda veličine sporiji kod nego što je potrebno
• Potpuno neprihvatljiv pristup za kompajlere
Prednosti:
• Jednostavnost
• Može da prevede svaki deo IR koda direktno u asembler u jednom prolazu
• Nikada ne mora da brine da će ostati bez dovoljno registara
• Ne mora da brine ni o pozivima funkcija ili o registrima specijalne namene
• Dobar je samo ako nam je potreban prototip kompajlera koji radi

47. Alokacija registara. Linarno skeniranje. Razlivanje registara.

Cilj: Pokušaj da držiš u registrim koliko god promenljivih možeš

Ta ideja bi trebalo da umanji broj ćitanja/pisanja u memoriju i uopšte ukupno korišćenje memorije
Da bi se to ostvarilo, potrebno je da se odgovori na naredna dva pitanja:
• U koje registre stavljamo promenjive?
• Šta se dešava kada nemamo dovoljno registara?

Konzistentnost registara:
• U svakoj tački programa svaka varijabla mora da bude na istoj lokaciji
◦ Ovo ne znači da svaka varijabla mora da bude na istoj lokaciji sve vreme, ali bez obzira
kojom putanjom smo došči očekujemo da nam je npr varijabla d u poslenjem bloku
prethodne slike u istom registru.
• U svakoj tački programa, svaki registar sadrži najviše jednu živu promenljivu
◦ Ovo znači da možemo da podelimo više varijabli istom registru ako nikada dve od njih
neće biti čitane zajedno
Živi opsezi i intervali

• Promenljiva je živa u nekoj tački u programu ako se njena vrednost čita pre nego što se u nju
ponovo nešto upiše
• Živost promenljivih se može pronaći korišćenjem globalne analize živosti
• Živi opseg promenljive je skup tačaka u programu u kojim je promenljiva živa
• Živ interval promenljive je najmanji interval IR koda koji sadrži sve žive opsege
promenljive
◦ Živ interval je manje precizan od živih opsega ali je sa njim lakše raditi.

Alokacija registara korišćenjem živih intervala

• Ako su dati živi intervali promenljivih u programu, možemo alocirati registre koristeći
jednostavan pohlepni algoritam
◦ Ideja: prati koji registri su slobodni u svakoj tački
◦ Kada živi interval počne, daj promenljivoj slobodan registar
◦ Kada se živi interval završi, oslobodi odgovarajući registar
◦ Problem: šta ako nemamo dovoljno registara?

Register spilling

Ako za promenljivu v ne postoji slobodan registar, onda je potrebno njenu vrednost prosuti.
Kada je promenljiva prosuta, ona se čuva u memoriji umesto u registru
Kada nam treba registar za promenljivu koja je bila prosutra potrebno je:
• da iselimo neki postojeći registar u memoriju
• da u taj registar učitamo vrednost prosute promenljive
• kada završimo, potrebno je da sadržaj registra vratimo u memoriju i da u taj registar vratimo
originalnu vrednost koja je tu bila.

Razlivanje je sporo, ali ponekad neophodno

Analiza algoritma:
• Prednosti:
◦ veoma je efikasan, nakon računanja intervala živosti, izvršava se u linearnom
vremenu
◦ Daje dosta kvalitetan kod
◦ Alokacija radi u jednom prolazu
◦ Često se koristi za JIT kompajlere
• Mane:
◦ Neprecizan jer koristi intervale a ne doseg
◦ Postoje bolje tehnike

*Algoritam se može poboljšati korišćenjem opsega živosti umesto intervala živosti.


48. Alokacija registara. Bojenje grafova. Čajtinov algoritam.

Graf međusobne zavisnosti registara (RIG) je neusmeren graf gde:


• Svaki čvor je jedna promenljiva
• Postoji ivica između dve promenljive koje su žive istovremeno u nekoj tački programa
• Alokacija registara se svodi na dodelu svakoj promenljivoj različitog registra u odnosu na
njegove susede
• Ovde postoji jedan problem...

Algoritam bojenja grafova:


Opisani problem je ekivalentan problemu bojenja grafova, koji je NP-težak problem ako ima bar tri
registra.

Ne postoji algoritam u polinomnom vremenu koji rešava ovaj problem.


Treba da se zadovoljimo nekakvom heuristikom koja daje dovoljno dobre rezultate za probleme koji
se javljaju u praksi.

Čajtinov algoritam

Intuicija:
• Pretpostavimo da pokušavamo da obojimo sa k boja graf. Pronađi čvor sa manje od k ivica
• Ako obrišemo ovaj čvor iz grafa i obojimo ono što preostane, možemo da nađemo bojenje i
za ovaj čvor kada ga vratimo nazad
• Razlog: za manje od k suseda, neka boja je sigurno neiskorišćena
Algoritam:
• Pronađi čvor sa manje od k izlaznih ivica
• Skloni ga iz grafa
• Rekurzivno oboj ostatak grafa
• Vrati čvor nazad
• Dodeli mu validnu boju

Problem: Šta se dešava ako ne možemo da pronađemo čvor sa manje od k suseda?


• Izaberi i ukloni proizvoljan čvor, označimo ga kao "problematičan"
• Kada vraćaš taj čvor, možda bude moguće da mu se dodeli ispravna boja
• U suprotnom, taj čvor će biti prosut u memoriju

Prednosti Čajtinovog algoritma:


• Za mnoge cfg pronalazi odlične dodele promenljivim registrima
• Pošto se promenljive razlikuju po korišćenju, proizvede se precizan graf međusobne
zavisnosti registara.
• Često se koristi u kompajlerima, npr GCCu
Mane:
• Osnovni pristup se zasniva na NP-teškom problemu bojenja grafa
• Heuristika može da dovede do patološke dodele najgoreg slučaja

Unapređenja algoritma:
• Izaberi promenljivu koja će biti prosuta koristeći neku pametnu heuristiku (najmanje
korišćena recimo)
• Nakon prosipanja promenljive izračunati opet rig zasnovan na tom prosipanju
49. Rasporedivanje instrukcija. Graf zavisnosti podataka.

Zbog procesorskog pipelining-a, redosled u kojem se instrukcije izvršavaju može da utiče na


performanse
Raspoređivanje instrukcija je pravljenje rasporeda instrukcija sa ciljem da se poboljšaju
performanse.
Svi dobri kompajleri podržavaju ovu optimizaciju.

Zavisnost među podacima

Zavisnost među podacima u mašinskom kodu je skup instrukcija čije ponašanje zavisi jedno od
druge.
Intuitivno, skup instrukcija koje ne mogu da se poređaju na drugaliji način.
Postoje tri vrste zavisnosti: čitanje nakon pisanja, pisanje nakon čitanja, pisanje nakon pisanja

Graf zavisnosti podataka

Graf koji prikazuje zavisnosti podataka u okviru osnovnog bloka naziva se graf zavisnosti podataka
To je direktan aciklični graf. Direktan, jer uvek jedna instrukcija zavisi od druge. Acikličan, jer nisu
moguće aciklične zavisnosti.
Mogu se rasporediti instrukcije u okviru osnovnog bloka u bio kom redosledu sve dok se ne
raspodele instrukcije tako da neka instrukcija prethodi svom roditelju.

Ideja: napravi topološko sortiranje zavisnosti podataka i poređaj instrukcije u tom redosledu.

Problem:
• Može postojati puno ispravnih topoloških udeđenja grafa zavisnosti podataka
◦ Kako izabrati onaj koji je dobar?
▪ U opštem slučaju, pronalaženje najboljeg rasporeda instrukcija je NP-težak problem
▪ U praksi se koriste heuristike

Raspoređivanje instrukcija

Moderni kompajleri mogu da rade značajno agresivnije raspoređivanje intrukcija sa ciljem da se


dobiju bolje performanse programa.
Primeri ovih tehnika su razmotavanje petlji i software pipelining.

50. Optimizacije koda zasnovane na upotrebi keša.

Pored optimizacije raspoređivanja instrukcija, mogu se vršiti i optimizacije transformacije koda sa


ciljem bolje upotrebe keša.
Upotreba keša se zasniva na dve vrste lokalnosti: vremenska i prostorna.
• Vremenska: Ako je nekoj memoriji skoro pristupano, verovatno će joj biti ponovo
pristupano uskoro.
• Prostorna: Ako je nekoj memoriji skoro pristupano, verovatno će i njeni susedni objekti
takođe biti korišćeni

Većina keš memorija je dizajnirano da iskoristi ove lokalnosti tako što se u kešu da drže skoro
adresirani objekti i tako što se u keš ubacuje i sadržaj memorije u blizini.
Poboljšanje lokalnosti
• Programeri obično pišu kod bez razumevanja o posledicama lokalnosti jer jezici ne
prikazuju detalje memorije.
• Neki kompajleri su sposobni da prepišu kod tako da se lokalnost iskoristi.
• Primer takve optimizacije je preraspoređivanje petlji.

Optimizacije koda
• Postoje i razne druge optimizacije koje se mogu sprovesti na nivou izgenerisanog koda

You might also like