INF-5110 Oppgaver kodegenerering, 7/5-09 Oppgave 1: Løs oppgavene 8.1 og 8.2 i Louden Oppgave 2: Løs oppgave 8.14.a i Louden. I stedet for oppgave 8.14.b, finn en tredje møte å implemetere switch/case på (ikke en serie tester og ikke en ren hopp-tabell) som kan være et godt kompromiss i mange tilfeller. Oppgave 3: Vi skal se på koden generert av TA-instruksjonene til høyre i figur 9.10 i detutdeltenotatet, side 539 (men finnesogsåpåsisteside i lysarkenefra5/5-09, dog med en trykkfeil: "SUB a, R1" skal rettes til "SUB c, R1"). a) Påvis at det finnes en bedre kodesekvens for de samme TA-setningene enn den angitte, som er generert av notatets algoritme. b) Diskuter hvordan vi kunne forandre denne kodegenererings-algoritmen, slik at den gir bedre kode i dette tilfellet. Oppgave 4: Løs Oppgave 4 fra eksamen 2007. Denne ligger på: http://www.uio.no/studier/emner/matnat/ifi/inf5110/v08/undervisningsmateriale/inf5110-eksamen2007.pdf Oppgave 5: a) Oversett (pr hånd) følgende setning til TA-kode: if a<b (c>d && e>=f) then x=8 else y=5 endif Oversett den til kode der alle hopp blir så direkte som over hodet mulig (uten å tenke på algoritmen for å gjøre det). b) Forsøk å tenke ut kodegenererings-algoritmer som kunne generere slik kode, f.eks. for setningen over.
Oppgave 1: Løs oppgavene 8.1 i Louden 2 + 3 + 4 + 5 : ------------------ t1 = 2 + 3 t2 = t1 + 4 t3 = t2 + 5 2 + (3 + (4 + 5)) : ------------------------ t1 = 4 + 5 t2 = 3 + t1 t3 = 2 + t2 a * b + a * b * c : ----------------------- t1 = a * b t2 = t1 * c t3 = t1 + t2 Her er altså den siste hårfint optimalisert (har oppdaget felles sub-uttrykk). En liten optimalisering av de to første (gjøre konstantbergeninger i kompiloatoren) ville lett optimalisert de to første til t1 = 14
Oppgave 1: Løs oppgavene 8.2 i Louden 2 + 3 + 4 + 5 : ------------------ ldc 2 idc 3 adi ldc 4 adi ldc 5 adi 2 + (3 + (4 + 5)) : ------------------------ ldc 2 idc 3 ldc 4 ldc 5 adi adi adi a * b + a * b * c : ----------------------- ld a ld b mpi dup //dupliserer toppen ld c mpi adi Her er igjen den siste hårfint optimalisert, ved at vi antar at det er en dup-instruksjon (duplisering) i P-kode. De to første kan selvfølgelig også optimaliseres som før til ldc 14
Oppgave 2 Løs oppgave 8.14.a i Louden. I stedet for oppgave 8.14.b, finn en tredje møte å implemetere switch/case på (ikke en serie tester og ikke en ren hopp-tabell) som kan være et godt kompromiss i mange tilfeller. a) Vitsen med en hopptabell er at man skal kunne gå rett inn i tabellen med caseindeksen, og at hopp-adressen står der. Det blir altså meget raskt, men tabellen må da spenne fra laveste til høyeste brukte indeks. Om det ikke er så mange brukte indekser, og avstanden mellom største og minste brukte indeks er stor kan tabellen bruke masse plass, men det meste av den vil være tom. Da kan en if-then-else-implementasjon med test på hvert tilfelle være like grei. Ny b) En tredje implementasjon kan være å sortere de brukte indeksverdiene, og så legge disse i en tabell med hopp-adressen ved siden av. Da kan man ved runtime bruke binærsøking til å finne om indeksen er der, og hvor det eventuelt skal hoppes.
Oppgave 3 Vi skal se på koden generert av TA-instruksjonene til høyre i figur 9.10 i det utdelte notatet, side 539 (men finnes også på siste side i lysarkene fra 5/5-09, dog med en trykkfeil: "SUB a, R1" skal rettes til "SUB c, R1"). a) Påvis at det finnes en bedre kodesekvens for de samme TA-setningene enn den angitte, som er generert av notatets algoritme. SVAR: Det dumme er at vi henter opp a to ganger fra lageret. Om vi allerede første gang hadde sett at vi trengte den en gang til så kunne vi med en gang kopiert den over i et annet register (for eksempel R1) før vi ødela verdien av a i R0 med instruksjonen SUB b, R0. Da ville de første instruksjonene bli: MOV a, R0 MOV R0, R1 // er altså billigere enn en MOV fra lager til register SUB b, R0 SUB c, R0 b) Diskuter hvordan vi kunne forandre denne kodegenererings-algoritmen, slik at den gir bedre kode i dette tilfellet. SVAR: Vi har jo allerede informasjon nok til å kunne gjøre denne optimaliseringen, i og med next use. Vi kan se at den første a -en har en next-use (og til og med at den har det allerede i neste injstruksjon), og da kan vi redde verdien unna i et ledig register om et slik finnes (med en MOV mellom registere)
Til oppgave 3: Eksempel på kode-generering Setninger Generert kode Reg. deskriptorer Adr. deskriptorer Alle Reg er ubrukte t = a - b MOV a, R0 R0 inneholder t t i R0 SUB b, R0 u = a c MOV a, R1 SUB c, R1 R0 inneholder t R1 inneholder u t i R0 u i R1 v = t + u ADD R1, R0 R0 inneholder v v i R0 R1 inneholder u u i R1 d = v + u ADD R1, R0 R0 inneholder d d i R0 og ikke i hukommelsen Avslutning av basal blokk MOV R0, d Alle Reg er ubrukte (Alle prog.variable i hukommelsen) 6
Oppgave 4a, eksamen 2007 Gitt følgende program, der alle setningene er tre-addresseinstruksjoner, bortsett fra at vi også tillater if- og while-setninger på vanlig måte. Instruksjonene x = input og output x regnes som vanlige tre-addresse-instruksjoner, med den opplagte betydning. Vi antar at ingen variable er i live ved slutten av programmet. 1: a = input 2: b = input 3: d = a + b 4: c = a * d 5: if ( b < 5 ) { 6: while ( b < 0 ) { 7: a = b + 2 8: b = b + 1 9: } 10: d = 2 * b 11: } else { 12: d = b * 3 13: a = d - b 14: } 15: output a 16: output d 4a Angi for hver av variablene a, b, c og d om de er i live eller ikke umiddelbart etter linje 4. Gi en kort forklaring for hver av variablene. Svar: a. Det kunne se ut som om denne var død, siden den settes ( defineres ) både i den ene og den andre grenen av if-setningen, uten å bli brukt først (linje 7 og linje 13). Men, dersom while-løkke går null ganger vil den verdien som a hadde etter linje 4 være den som brukes i linje 15. Altså er den i live. b. Denne er i høyeste grad live, siden den brukes allerede i linje 5 c. Dennebrukesi detheletattikkeetterlinje4, oger derfor ikke i live (altså død). d. Denne er ikke i live, siden den helt sikkert settes i begge if-grener, uten å bli brukt først (linje 10 og 12).
Oppgave 4b, eksamen 2007 4b Forklar hva man i kodegenererings-algoritmen i kap 9.6 (i den utdelte kopien fra ASU-boka) mener med at en variabel har en neste bruk ( next use ) og hvordan dette skiller seg fra å være i live. Forklar hvorfor denne algoritmen skiller mellom disse to begrepene. Svar: Begrepet neste bruk er, i motsetning til begrepet i live knyttet til basale blokker. En variabel har på et gitt sted i programmet en neste bruk dersom den verdien den der har kan bli brukt senere i den basale blokka vi nå er i. (da kan man også si at verdien til variabelen vil bli brukt senere i blokka, siden alt i en basal blokk utføres pent etter hverandre). Grunnen til at algoritmen i notatet er interessert i neste bruk (i den basale blokka) er at den har som grunnfilosofi at ingen variable som er i live skal ligge i et register (men i hjemmeposisjonen sin) mellom basale blokker. Det gjøres altså ikke noe forsøk på å optimalisere over flere basale blokker. Dermed, om en ikke har noen neste bruk i blokka, så er det ingen fordeler med å la den ligge i et register (men om den er i live må man sørge for at verdien ligger i hjemmeposisjonen). (NB: Ordet hjemmeposisjon er ikke brukt i boka. Der snakker man heller om å ha variabel-verdier i et register, og to store them, når de må kopieres til sin variabel-posisjon.)
Oppgave 5 a) a) Oversett (pr hånd) følgende setning til TA-kode: if a<b (c>d && e>=f) then x=8 else y=5 endif Oversett den til kode der alle hopp blir så direkte som over hodet mulig (uten å tenke på algoritmen for å gjøre det). SVAR: t1 = a < b if_true t1 goto 1 // Vi vet at uttrykket er sant t2 = c > d if_false t2 goto 2 // Vi vet at uttrykket er galt t3 = e >= f if_false t3 goto 2 // Vi vet at uttrykket er galt, ellers er det sant og vi fortsetter label 1 x = 8 goto 3 label 2 y = 5 label 3
Oppgave 5 b) b) Forsøk å tenke ut kodegenererings-algoritmer som kunne generere slik kode, f.eks. for setningen over. SVAR: Hovedsaken med den koden som er angitt i 5 a) er altså at man aldri beregner den logiske verdien på hele det boolske uttrykket, og at så fort man har nok informasjon til å vite om uttrykket blir true eller false, så hopper man direkte til den grenen der dette tilfellet skal behandles. Hvordan slik kode kan genereres vil avhenge veldig om vi bare skal ha symbolske labeler som vi kan velge fritt, eller om hoppene skal gå til faktiske fysiske adresser, f.eks. til der else-grenen starter (som ikke er kjent når det boolske uttrykket behandles). Det siste tilfellet er verst. Da kan man generere hopp med tomme adresser der man vet det skal inn hopp til f.eks. en else-gren, og det kan for samme else-gren bli mange slike hopp etter hvert. Man må da holde lister med adressene til alle de hopp som skal gå for eksempel til then-grenen og til else-grenen, og når vi en gang kommer til disse grenene (og vet deres start-adresse) må man gå tilbake i listen og sette inn adressen i alloe de rette hopp-instruksjonene. Merk at man da også må være nøye med om det er på true eller false man skal hoppe
Litt om Javas class-filer og byte-kode Ikke pensum, men anbefales lest som bakgrunnsmateriale Disse formatene ble planlagt fra start som en del av hele Javaideen Byte-koden gir portabilitet ved at den utføres av en interpretator (som må skrives for hver enkelt maskin, gjerne i C eller C++) Gir, sammen med et standard bibliotek, et enhetlig grensesnitt til operativsystemet, grafikk, osv. fra Java Samme Java-kompilator kan dermed brukes på alle maskiner For effektivitet: Byte-koden blir nå ofte oversatt til maskinkode før utførelse, enten i en egen kompilerings-operasjon, eller som JITkompilering som en del av loadinga. Likner mye på den lavnivå-koden dere oversetter til i Oblig 2 Men hos dere blir også Loading gjort samtiding som koden blir laget. I Javas byte-kode blir alle navn beholdt på tekstlig form i den bytekoden som legges på fil (som klasse-filer) Først når denne taes inn av loaderen blir den laget om til binær kode. 11
Litt om Javas class-filer og byte-kode Formatet av class-filene inneholder både Den utførbare koden (som byte-kode = sekvenser av byteinstruksjoner), Og hele strukturen av klassen, med info om navn, variable, metoder, parametere, typer, etc. Disse to informasjonstypene ligger tradisjonelt på to forskjellige filer: For C og C++, på.c-filer (som oversettes til maskinkode) og på.h-filer En class-fil leses derved i to sammenhenger i forbindelse med kompilering/kjøring: Når en annen klasse som referer til denne (f.eks. en subklasse) kompileres: Da ser man mest på struktur-delen av klasse-filen Når klassen skal loades: Da ser man mest på byte-koden for hver av metodene i klassen 12
Formatet av Javas class-filer På hver class-fil er det bare beskrivelse av én klasse eller ett grensesnitt class-filene har all informasjon om: Navn på klassen, og på superklassen og implementerte grensesnitt Hvilke variable klassen har, ved navn, type og synlighet Hvilke metoder den har, ved navn, type, synlighet, parametere (med typer), og byte-kode-sekvens class-filene har også et navne-område eller et konstant-område Her ligger alle tekster, navn, og tall-verdier (på tekstlig form) som brukes i programmet, pent etter hverandre. Når disse skal brukes ellers i klassefila (f.eks. i selve byte-koden) så angir man bare indeksen til dette navnet i navne-området. 13
Formatet av byte-koden Byte-koden har samme idé som P-koden, ved at arbeids-dataene skal ligge på en stakk under utførelsen Funksjons-delen av instruksjonen er på én byte, altså plass til 256 instruksjoner (som faktisk er litt lite) Byte-instruksjonenes adressefelt (der dette trengs) angis ved fullt navn (tekstlig), type og klasse-tilhørighet Det eneste unntaket fra dette er lokale variable i metoder. Disse angis som relativ-adresse (i byte) i aktiverings-blokken. Som antydet på forrige foil: Det blir mange navn det stadig skal refereres til. Derfor ligger navnene bare én gang hver i et eget navne-område, og de angis andre steder bare ved en indeks inn i dette området 14
Hva foregår i en Java Virtual Machine (JVM) En JVM kan enten interpretere eller oversette til maskinkode (JIT, eller vanlig) Den består av en loader, en verifikator, samt av en interpretator eller en oversetter Loaderen starter med å lese inn og behandle den angitte class-fila Leser så etter hvert inn alle class-filer som det referers til fra denne, osv. Lager en descriptor for hver klasse. Denne vil ligge fast under den kommende utførelse av programmet Alle objekter har en peker til descriptoren for sin klasse Descriptoren inneholder virtuell-tabellen for klassen, en peker til descriptoren for superklassen, etc. Dersom debugging eller refleksivitet: Descriptoren inneholder også info om alle variable/metoder Descriptoren kan også ha en peker til et sted der selve Java-koden ligger 15
Mer om: Hva foregår i en JVM Loaderen gjør allokering av variable i klassene, dvs.: Går gjennom byte-kode-sekvensen og gjør hver av de tekstlige operandene om til relativadresser, og alle klasse-angivelser (f.eks. i casting og ved new C ) om til pekere til klassens descriptor Merk at allokering av én klasse må gjøres før allokering for dens subklasser Dersom interpretering: Legger sekvensen av byte-instruksjoner (nå med tall både for funksjonsangivelse og adresser) ut i et passelig format Starter å interpretere dette Dersom oversetting: Oversetter sekvensen byte-instruksjoner til maskinkode for gitt maskin Kan også gjøres som en vanlig forhåndskompilering, eller en JIT-kompilering (Just-In-Time) i forbindelse med at programmet skal startes opp. Kan også gjøre noe midt i mellom: Interpretere først, men etter hvert oversette de metoder som brukes mest. Finnes også Java-kompilatorer som hopper over hele byte-kode-steget Får da en tradisjonell kompilator, som kan lage effektiv kode Men det er mer problematisk å koble seg til Javas standard-bibliotek etc. 16
Verifikatoren Denne kan gå gjennom klassefiler og sjekke at de er konsistente Spesielt sjekke at bytekoden er konsistent (se under) Og at den ikke gjør noe den ikke har autorisasjon til Dette er spesielt aktuelt for class-filer som hentes inn over nettet Hva gjør verifikatoren med byte-koden: Simulerer hele tiden hva som vil ligge på stakken under utførelsen Sjekker at det som ligger på toppen av stakken hele tiden stemmer typemessig etc. med den instruksjonen som skal utføres Sjekker at når det gjøres hopp så er stakken helt lik der det hoppes fra og der det hoppes til. Sjekker at de operasjonene som gjøres er lovlige (i henhold til autorisasjon) 17
Java-program: Typisk Byte-kode, ferdig til interpretering: Merk: Også funsjonskodene (f.eks. sipush, iconst_2 og goto) er nå gjort om til tallkoder (mellom 0 og 255) outer: for (int i = 2; i < 1000; i++) { for (int j = 2; j < i; j++) { if (i % j == 0) continue outer; } System.out.println (i); } 0: iconst_2 1: istore_1 // i har reladr 1 2: iload_1 3: sipush 1000 6: if_icmpge 44 9: iconst_2 10: istore_2 // j har reladr 2 11: iload_2 12: iload_1 13: if_icmpge 31 16: iload_1 17: iload_2 18: irem # remainder 19: ifne 25 22: goto 38 25: iinc_2, 1 28: goto 11 31: getstatic #84; // Området for println()? 34: iload_1 35: invokevirtual #85; // println() 38: iinc 1, 1 41: goto 2 44: return 18