PG4200 Algoritmer og datastrukturer Lab 1 8.januar 2014 Innledning I dag skal vi undersøke en rekke velkjente databeholdere i Java: java.util.arraylist java.util.linkedlist java.util.hashset java.util.treeset Vi bruker vanligvis disse klassene dersom vi trenger å holde orden på en haug med objekter. Her vil vi legge vekt på noen viktige operasjoner disse databeholderne har til felles: (1) Ta vare på objekter, altså lagring. (2) Finne tilbake til objekter, altså søk. (3) Å kunne fjerne objekter, alstå sletting Dette tas hånd om av metodene (1) add (For å lagre) (2) contains (For å søke) (3) remove (For å slette) De fire klassene ArrayList, LinkedList, HashSet, TreeSet implementerer alle disse tre metodene, som hører til grensesnittet java.util.collection. Siktemål: Vi ønsker å måle tidsbruken for kall av disse metodene. Vi ønsker å sammenligne ytelsen til de fire klassene. 1
Hvilke data skal vi teste metodene på? Datastrukturene: Vi vil arbeide med beholdere for String-objekter. Beholderne initialiseres slik initialiseres slik: Collection<String> arraylist = new ArrayList<String>(); Collection<String> linkedlist = new LinkedList<String>(); Collection<String> hashset = new HashSet<String>(); Collection<String> treeset = new TreeSet<String>(); Disse fire klassene hører alle med i pakken java.util, så vi kan importere dem ved å skrive import java.util.*;. Vi legger merke til at disse klassene er definert ved hjelp av generiske typer 1. Tekstene: I dag har vi funnet fram Shakespeares samelede verker, samt Bibelen og Moby Dick. Disse tekstene er lagret på rekke og rad i filen SMB.txt. I tillegg har vi til rådighet en fil med mange nonsense-ord 2, nemlig nonsense.txt. Faglærer har laget metoder for å lese disse tekstene ord for ord inn i String[]-tabeller: String[] SBM = readwords("sbm.txt"); String[] nonsense = readwords("nonsense.txt"); I tillegg har vi til rådighet en metode som lar oss lagre et visst antall elementer fra SMB i objekter som impleneterer Collection<String>-grensesnittet: private static void prepare(collection<string> collection,int size){ collection.clear(); for (int i = 0; i < size; i++) collection.add(sbm[i]); } Metoder Måling av tid Vi bruker metoden java.lang.system.nanotime() til å måle tiden. Denne metoden returnerer en long-verdi som angir antallet nanosekunder etter et bestemt (men dog ukjent) tidspunkt. Vi kan da måle tid på følgende måte: 1 Vi angir hvilken type objekter vi ønsker å lagre i skarpe klammer, som i uttrykket ArrayList<String>(). Dersom vi ønker å erstatte String-objekter med Integer-objekter, så må vi istenfor skrive ArrayList<Integer>(). Vi skal lære mer om generiske typer siden. Generisk programering er for øyeblikket ikke vesentlig. 2 Spørsmål: Hvorfor trenger vi nonsense-ord? Svar: Vi kan bruke dem til å undersøke hvor lang tid det tar å søke etter ord som ikke finnes, og til å svare på om det spiller noen rolle for tidsbruken om ordet finnes eller ikke. 2
long start = System.nanoTime(); dosomehardwork(); long stop = System.nanoTime(); int timeusage = (int) (stop-start) Vi har noen utfordringer: Et enkelt kall av de metodene vi er interssert i (add, remove, og contains) kan ta veldig kort tid. Metoden nanotime oppgir svaret med høy presisjon, men det gis ingen garanti for at svaret er nøyaktig. En måte å håndtere dette på er å måle vi hvor lang tid det tar å utføre et stort antall operasjoner, for så å beregne gjennomsnittet: long start = System.nanoTime(); for(int i = 0 ; i < numberoftrials; i++) collection.contains(targets[i]) long stop = System.nanoTime(); return (int) (stop-start)/numberoftrials; // Cast from long to int. Feilkilder 1 Andre operasjoner: Vi ønsker å måle hvor hvor mye tid programmet bruker i contains-metoden. Det foregår dog noen andre ting i forbindelse med tidsmålingen. Operasjoner knyttet til for-løkken: En test: i < numberoftrials. En aritmetisk operasjon: i++. Vi henter fram objektet targets[i]. Om man vil, så kan man undersøke disse feilkildene. Man vil sannsynligvis finne at de er relativt ubetydelige. 2 Systematiske feil: For å unngå systematiske feil, f.eks at vi arbeider med strenger som det tilfeldigvis er enkelt å søke etter, er det en fordel å randomisere arbeidsdataene. Vi løser dette ved å bruke en metode for å generere tilfeldige utvalg: 3
private static String[] randomsample(string[] source, int samplesize){ final Random random = new Random(); String[] output = new String[sampleSize]; } for (int i = 0; i < samplesize; i++) output[i] = source[random.nextint(source.length)]; return output; Denne velger ut tilfeldige elementer av tabellen source og returnerer et String[]- objekt med samplesize elementer. Metodekallet randomsample(sbm,100) velger f.eks ut 100 tilfeldige ord fra Shakespeare, Bibelen og Moby Dick. 3 Akselerasjon: Det finnes dog noen feilkilder som det er vanskelig å holde oversikt over: Hvordan java-miljøet arbeider. Hvordan datamaskinens hardware fungerer. Javas innebygde optimaliseringsstrategier. Noen stikkord er caching og effektivisering av operasjoner som foregår hyppig. Hvis disse effektiviseringsmetodene faktisk virker slik de skal, bør vi forvente at operasjoner som utføres hyppig vil gå raskere. En måte å håndtere disse effektene er å prøvekjøre testene for å varme opp maskinen før vi faktisk måler tidsbruken. Noen spørsmål av interesse: Hvor lang tid tar det å sette inn et objekt i en collection som inneholder 100 objekter fra før? Hvor lang tid tar det å søke etter elementer i en collection som inneholder 100 objekter? Hvor lang tid tar det å sette inn et objekt i en collection som inneholder 10000 objekter fra før? Hvor lang tid tar det å søke etter elementer i en collection som inneholder 10000 objekter? Hvordan varierer tidsbruken med antall objekter i vår collection? Når vi søker etter et objekt, har det noe å si om objektet er der fra før eller ei? Når vi lagrer et objekt, har det noe å si om objektet er der fra før eller ei? Nøkkelspørsmål: Hvordan varierer kjøretiden for de ulike operasjonene som fuksjon av antall objekter inneholdt i beholderen? 4
Arbeidsoppgaver 1 Måle tidsbruk Lage tall. Sett opp et java-program der du tester kjøretiden til de ulike klassene. Ta gjerne utgangspunkt i CollectionTest.java som følger vedlagt, og som du også finner her: home.nith.no/ sydlar/pg4200/kode/uke1/collectiontest.java Man kan la programmet lage output slik: System.out.printf("%d\t%d%n",sizeOfCollection,timeUsage); Dermed vil programmet skrive ut to kolonner med tall, eller man kan lage noe i retning av size cont. rem. add type 511 1765 1819 90 java.util.arraylist 1084 3807 4162 98 java.util.arraylist 1738 6564 7150 147 java.util.arraylist 2486 7683 8114 133 java.util.arraylist 3341 11388 17048 268 java.util.arraylist 4318 13723 17774 357 java.util.arraylist 2 Analysere tallene Grafisk fremstilling Vi kan bruke et regneark til å hjelpe oss med å forstå tallene. 3 Når vi har tallene lagret i kolonner i et regneark kan vi lage et plott av typen xy-scatter, gjerne med heltrukne linjer. Denne figuren viser en grafisk fremstilling av kjøretid som funksjon av antall objekter i beholderen: 3 Når vi har fått programmet til å spy ut kolonner med tall, er det vanligvis enkelt å få tallene lastet inn i et regneark. I de fleste regneark vil det fungere å bruke copy-paste. 4 5
Vi kan bruke den grafiske fremstillingen til å svare på følgende spørsmål: Hvor raskt går det å søke i de ulike Collection-objektene? Hvor raskt går det å lagre objekter i de ulike Collection-objektene? Hvordan utvikler tidsbruken seg i forhold til antallet objekter i de ulike Collection-objektene? Øker kjøretiden? Hvor fort? Avtar kjøretiden? Hvor fort? Er kjøretiden konstant? 3 Kritikk av målingene (1) Akselerasjonseffekten: (Se punkt 3 i avsnittet over om feilkilder) Undersøk om det virkelig er slik at operasjonene går raskere når de gjentas mange ganger. Vurder om dette har noen betydning for konklusjonene under punkt 2 over. (2) Randomisering: Undersøk om du får andre resultater dersom du gjør forsøk uten randomisering. (Se punkt 2 i avsnittet om feilkilder) (3) Antallet forsøk: Gjør mange forsøk med ulike verdier for numberoftrials og registrer kjøretiden. Tips: I hver kjøring kan du la numberoftrials være et tilfeldig tall mellom f.eks 0 og 100. Dette motvirker eventuelle forstyrrelser fra akselerasjonseffekten. Random r = new Random(); for(int i = 0; i < 100; i+=1) { int numberoftrials = r.nextint(1000); /*...*/ (4) Hvor mye forstyrrer for-løkka og tabelloppslaget målingene? (Se punkt 1 i avsnittet over om feilkilder) 6