Upload
others
View
6
Download
0
Embed Size (px)
Citation preview
1
INF2440 Uke 10, v2015 :
Arne Maus PSE,
Inst. for informatikk
Hva så vi på i uke 9
2
Et sitat om tidsforbruk ved faktorisering Om en feil i Java 7 ved tidtaking Hvordan parallellisere rekursive algoritmer Gå ikke i ‘direkte oversettelses-fella’
eksemplifisert ved Kvikk-sort, 3 ulike løsninger Oblig3 – det sekvensielle tilfellet
Bla. bør lage klasse IntList (hvis tid) Hvor lang tid tar de ulike mekanismene vi har i
Java ?
Hva skal vi se på i Uke10
Hvor lang tid de ulike mekanismene vi har i Java tar. Java 6 (2012) og Java 8 (2015)
Automatisk parallellisering av rekursjon PRP- Parallel Recursive Procedures
Nåværende løsning (Java, multicore CPU, felles hukommelse) –implementasjon: Peter L. Eidsvik
Mange tidligere implementasjoner fra 1994 (C over nettet, C# på .Net, Java over nettet,..)
Demo av to kjøringer Hvordan ikke gå i fella: Et Rekursivt kall = En Tråd
Halvautomatisk
Hvordan kan vi bygge en kompilator (preprosessor) for automatisk parallellisering Prinsipper ( bredde-først og dybde-først traversering av r-treet) Datastruktur Eksekvering
Krav til et program som skal bruke PRP 3
Hvor lang tid tar egentlig ulike mekanismer i Java ? (fra Petter A. Busteruds master-oppgave: Invesigating Different
Concurrency Mechanisms in Java) - i 2012
Dette er målinger på en relativt ny CPU (2009): Intel Pentium i7 920 GHz– 4 core 2 GB DDR2 RAM @ 533 MHz
Windows 7 32bit og Java 6 (med noe HotSpot kompilering) Linux Mint 13, 32bit Det er de relative hastighetene som teller Petter målte bl.a. :
kalle en metode lage et nytt objekt (new C() ) + kalle en metode lage en Tråd (new Thread + kalle run() ) Lage en ExecutorService + legge inn tråder Tider på int[] og trådsikre AtomicIntegerArray
4
Generelt er Linux 15-20% raskere enn Windows (ikke optimalisert)
Hvor lang tid i µs tar egentlig ulike mekanismer i Java ? (fra Petter A. Busteruds master-oppgave: Investigating Different
Concurrency Mechanisms in Java)
5
A) Metodekall på Windows og Linux (ikke optimalisert)
Generelt er kan vi gjøre mer enn 500-700 metodekall per millisek.
Å lage et nytt objekt av en klasse (new) og kalle en metode i objektet (µs)
6
Konklusjon: - Verken det å kalle en metode eller å lage et objekt tar særlig lang tid - Mye blir optimalisert videre - Forskjellen mellom første og andre kall er at det sannsynligvis er blitt
JIT-kompilert til maskinkode, men ikke optimalisert
Object
7
Lage en tråd (new Thread()) og kalle run() - µs
Lage en ExecutorService
8
Lage og bruke en vanlig int [] a (IKKE trådsikker) mot AtomicIntegerArray (trådsikre heltall)
9
Oppsummering om kjøretider fra Petter: Metodekall tar svært liten tid: 2-5 µs og kan
også optimaliseres bort (og gis speedup >1) Å lage et objekt av en klasse (og kalle en
metode i det) tar liten tid: ca.785 µs Å lage en tråd og starte den opp tar en del tid:
ca.1500 µs, men lite, ca. 180 µs for de neste trådene (med start() )
Å lage en en tråd-samling og legge tråder (og Futures) opp i den tar ca. 7000 µs for første tråd og ca. 210 µs for neste tråd.
Å bruke trådsikre heltall (AtomicInteger og AtomicIntegerArray) mot vanlige int og int[] tar vanligvis 10-20 x så lang tid , men for mye bruk (> 106 ) tar det bare ca. 2x tid (det siste er sannynligvis en cache-effekt)
10 En parallell løsning må alltid sammenlignes med hvor mye vi kunne ha gjort (f.eks sortert) sekvensielt på den samme tida!
Nye målinger i Java8 i 2015 – metodekall-eks. mm.
11
//Metodekall s ="Metodekall"; t = System.nanoTime(); k=neste(k); d = (double) ((System.nanoTime() -t)/1000.0); println(s+Format.align(d,10,4)+"us. forste gang"); t = System.nanoTime(); for (int i = 0; i<n; i++) { k+=neste(k); } d = (double) ((System.nanoTime() -t)/(n*1000.0)); println(s+Format.align(d,10,4)+"us.snitt " + n+" ganger");
// Koden de andre tilfellene som ble testet ia = new int[100]; // new Array (t1 = new Thread(new Arbeider(3))).start(); // new Thread start&join() try{t1.join();}catch (Exception e){} k= new C(k).les(); // lage Object C + metodekall k= new D(k).les(); // lage Object D+ metodekall ia[i%100] =i; // skriv i array k = ia[i%55]; // les fra array
Hva med i dag (Java 8)– har kjøretidene endret seg ?
Tid første (us)
Tid neste (us)
Snitt tid 10 (us)
Snitt tid 100000 (us)
Speedup
Metodekall 3.15 0.7 0.14 0.034 92
new int[100] 0.67 1.4 0.49 0.32 2
new Thread() start()+join()
4156.0 202.0 391.0 92.7 44
new C() + metodekall
3214.0 1.05 0.28 0.08 40175
new D() + metodekall
4497.0 1.4 0.70 0.11 40881
Ett Array write/read
0.35 0.24 0.24 0.03 12
12
Konklusjoner: !) Ulik speedup. Best: å lage objekter, og metodekall 2) relativt lite speedup på arrayer Det er vår program som blir raskere, IKKE JVM (java) som tolker det bedre. De på det å lage objekter av en klasse C, gjør ikke noe for klasse D-objekter
Sammenlignet med Petters tider? Java8: Tid første (us)
Java8 Snitt tid neste 10 (us)
Java 6: Tid første (us)
Java6: Snitt tid 16 (us) Uten join()
Metodekall 3.15 0.14 4.75 1.96
new int[100] 0.67 0.49
new Thread() start()+join()
4156.0 391.0 1814 284
new C() + metodekall
3214.0 0.28 770 53
new D() + metodekall
4497.0 0.70 770 53
Ett Array write/read
0.35 0.24 0.27
13
Konklusjoner: !) Ulik speedup. Best: å lage objekter, og metodekall 2) relativt lite speedup på arrayer Det er vår program som blir raskere, IKKE JVM (java) som tolker det bedre. De på det å lage objekter av en klasse C, gjør ikke noe for klasse D-objekter
2) Generelt om rekursiv oppdeling av a[] i to deler
14
void Rek (int [] a, int left, int right) { <del opp omradet a[left..right] > int deling = partition (a, left,right); if (deling - left > LIMIT ) Rek (a,left,deling-1); else <enkel løsning>; if (right - deling > LIMIT) Rek (a,deling, right); else <enkel løsning> }
void Rek(int [] a, int left, int right) { <del opp omradet a[left..right]> int deling = partition (a, left,right); Thread t1 = null, t2= null; if (deling - left > LIMIT ) t1 = new Thread (a,left,deling-1); else <enkel løsning>; if (right - deling > LIMIT) t2 = new Thread (a,deling, right); else <enkel løsning> try{ if (t1!=null)t1.join(); if (t2!=null)t2.join();} catch(Exception e){..}; }
Hvor mange kall gjør vi i en rekursiv løsning?
Anta Quicksort av n =2k tall (k= 10 ⇒ n = 1000, k= 20 ⇒ n= 1 mill)
Kalltreet vil på første nivå ha 2 lengder av 219, på neste: 4 = 22 hver med 218 og helt ned til nivå 20, hvor vi vil ha 220 kall hver med 1 = 20 element.
I hele kalltreet gjør vi altså 2 millioner -1 kall for å sortere 1 mill tall !
Bruker vi innstikksortering for n < 32 = 25
så får vi ‘bare’ 220-5 = 215 = 32 768 kall. Metodekall tar : 0.2-5 µs og kan også
optimaliseres bort (og gis speedup >1) Å lage en tråd og starte den opp tar:
ca.1500 µs, men ca. 180 µs for de neste trådene (med start() og join())
15
nivå: 0 1 2
20
Vi kan IKKE bare erstatte rekursive kall med nye tråder i en rekursiv løsning !
Konklusjon om å parallellisere rekursjon
Antall tråder må begrenses ! I toppen av treet brukes tråder (til vi ikke har flere og
kanskje litt mer) I resten av treet bruker vi sekvensiell løsning i hver tråd! Viktig også å kutte av nedre del av treet (her med insertSort)
som å redusere treets størrelse drastisk (i antall noder) Vi har for n = 100 000 gått fra:
Speedup > 1 og ca. 10 000x fortere enn ren oversettelse. 16
n sekv.tid(ms) para.tid(ms) Speedup 100000 34.813 41310.276 0.0008 Ren trådbasert 100000 8.118 823.432 0.0099 Med insertSort 100000 7.682 5.198 1.4777 + Avkutting i toppen
Drømmen om lage automatisk parallelliseing
Parallellisering gir lang og vanskelig å lage kode Det finnes særlig to typer av sekvensielle programmer
som tar ’lang’ tid: A) Med løkker (enkle, doble,..) B) Rekursive
Drømmen er man bare helt automatisk, eller bare med noen få kommandoer kan oversette et sekvensielt program til et parallelt. Med løkker hadde vi bl.a HPFortran (Fortran90) som
paralleliserte løkker (slo ikke helt an) Reursive metoder – vi skal se på PRP (et system jeg har fått
lager som hovedfagsoppgaver flere ganger siden ca. 1995)
17
PRP: Den grunnleggende ideen: – Rekursjonstreet og treet av tråder er ‘like’, men
18
A
B
D
C
E F G
A
B
D
C
E F G
Dybde først Bredde først
1
Rekursjon
2
3 4
Tråder
5
6 7
8 1 2
3 4
6 5
Tråder: - Kan ikke bruke ‘svaret’ fra venstre-tråd til å lage parametere for høyre-tråd - Venter på trådene (og ‘svaret’) først når begge trådene er sendt ut..
Fra rekursjon til parallelle prosedyrer (metoder)
19
A
B
D
C
E F G
Rekursjon
A
B
D
C
E F G
Kjerne 0
Kjerne 2 Kjerne 1
Kjerne3 Kjerne 4
Multikjerne CPU
I Uke 9 så vi på å overføre Rekursjon til tråder - her fra Peter Eidsviks masteroppgave Vi skal nå automatisere det Vi lager en preprosessor: javaPRP
dvs. et Java-program som leser et annet Java-program og omformer det til et annet, gyldig Java-program (som er det parallelliserte programmet med tråder)
For at JavaPRP skal kunne gjøre dette, må vi legge inn visse kommentarer i koden: Hvor er den rekursive metoden Hvor er de rekursive kallene
Bare rekursive metoder med to eller flere kall, kan paralllelliseres . 20
Et eksempel før mer ‘teori’ med en kjørbar sekvensiell Quicksort
21
import java.util.Random; class QuicksortProg{ public static void main(String[] args){ int len = Integer.parseInt(args[0]); for(int i = 0; i < 11; i++){ int[] arr = new int[len]; Random r = new Random(); for(int j = 0; j < arr.length; j++){ arr[j] = r.nextInt(len-1); } long start = System.nanoTime(); int[] k = new QuicksortCalc().quicksort (arr,0,arr.length-1); long timeTakenNS = System.nanoTime() - start; System.out.println(timeTakenNS/100000.0); } } }
Nesten helt vanlig QuickSort – vi har riktignok pakket den inn i en klasse Vi kompilerer og kjører den og tar tiden (11 ganger)
import java.util.Random; class QuicksortProg{ public static void main(String[] args){ int len = Integer.parseInt(args[0]); int [] tid = new int[11]; for(int i = 0; i < 11; i++){ int[] arr = new int[len]; Random r = new Random(); for(int j = 0; j < arr.length; j++){ arr[j] = r.nextInt(len-1); } long start = System.nanoTime(); int[] k = new QuicksortCalc().quicksort (arr,0,arr.length-1); long timeTakenNS = System.nanoTime() - start; tid[i] = (int) timeTakenNS/1000000; System.out.println(timeTakenNS/100000.0); } tid = QuicksortCalc.insertSort(tid,0,10); System.out.println("Median sorteringstid for 11 gjennomlop:"+tid[5]+"ms. for n="+len); } }
class QuicksortCalc{ int INSERT_LIM = 48; int[] quicksort (int[] a, int left, int right){ if (right-left < INSERT_LIM){ return insertSort(a,left,right); }else{ int pivotValue = a[(left + right) / 2]; swap(a, (left + right) / 2, right); int index = left; for (int i = left; i < right; i++) { if (a[i] <= pivotValue) { swap(a, i, index); index++; } } swap(a, index, right); int index2 = index; while(index2 > left && a[index2] == pivotValue){ index2--; } a = quicksort (a, left, index2); a = quicksort (a, index + 1, right); return a; } }
QuickSort av 10 mill tall (ca. 0.95 sek) sekvensielt
22
M:\INF2440Para\PRP>java QuicksortProg 10000000 919.320237 948.897802 950.035171 946.001883 937.006513 940.43017 1027.33572 995.356381 1011.87974 934.688378 957.091764 Median sorteringstid for 11 gjennomlop:948ms. for n=10000000
Nå legger vi til tre kommentarer så den kan preprosessereres over i en parallell versjon (/*REC*/ og /*FUNC*/ ):
23
import java.util.Random; class QuicksortProg{ public static void main(String[] args){ int len = Integer.parseInt(args[0]); int [] tid = new int[11]; for(int i = 0; i < 11; i++){ for(int i = 0; i < 11; i++){ int[] arr = new int[len]; Random r = new Random(); for(int j = 0; j < arr.length; j++){ arr[j] = r.nextInt(len-1); } long start = System.nanoTime(); int[] k = new QuicksortCalc().quicksort(arr,0,arr.length-1); long timeTakenNS = System.nanoTime() - start; tid[i] = (int) timeTakenNS/1000000; System.out.println(timeTakenNS/100000.0); } tid = QuicksortCalc.insertSort(tid,0,10); System.out.println("Median sorteringstid for 11 gjennomlop:"+tid[5]+"ms. for n="+len); } }
class QuicksortCalc{ int INSERT_LIM = 48; /*FUNC*/ int[] quicksort(int[] a, int left, int right){ if (right-left < INSERT_LIM){ return insertSort(a,left,right); }else{ int pivotValue = a[(left + right) / 2]; swap(a, (left + right) / 2, right); int index = left; for (int i = left; i < right; i++) { if (a[i] <= pivotValue) { swap(a, i, index); index++; } } swap(a, index, right); int index2 = index; while(index2 > left && a[index2] == pivotValue){ index2--; } /*REC*/ a = quicksort(a, left, index2); /*REC*/ a = quicksort(a, index + 1, right); return a; } }
Kompilér JavaPRP -systemet, så start det
24
M:\INF2440Para\PRP>javac JavaPRP.java M:\INF2440Para\PRP>java JavaPRP
Det starter et GUI-interface
25
26
Trykker: Choose a file
27
28
Trykket så Compile: - Kompilerte da den
parallelliserte filen: QuicksortProgPara.java
- Legger så inn parameter (10 mill) på kommandolinja og velger Execute.
29
Resultatet kommer i log-vinduet. -en OK speedup for den parallelle utførelsen: S=948/525= 1,80
Oversettelsen : Quicksort
30
import java.util.Random; class QuicksortProg{ public static void main(String[] args){ int len = Integer.parseInt(args[0]); for(int i = 0; i < 11; i++){ int[] arr = new int[len]; Random r = new Random(); for(int j = 0; j < arr.length; j++){ arr[j] = r.nextInt(len-1); } long start = System.nanoTime(); int[] k = new QuicksortCalc().quicksort (arr,0,arr.length-1); long timeTakenNS = System.nanoTime() - start; System.out.println(timeTakenNS/100000.0); } } }
class QuicksortProgPara{ public static void main(String[] args){ new Admin(args); } } class Admin{ public Admin(String[] args){ initiateParallel(args); } void initiateParallel(String[] args){ int len = Integer.parseInt(args[0]); for(int i = 0; i < 11; i++){ int[] arr = new int[len]; Random r = new Random(); for(int j = 0; j < arr.length; j++){ arr[j] = r.nextInt(len-1); } long start = System.nanoTime(); int[] k = startThreads (arr,0,arr.length-1); long timeTakenNS = System.nanoTime() - start System.out.println(timeTakenNS/100000.0); } }
Starten på brukerens kode (77 linjer) Starten på oversatt kode (245 linjer)
Dette kan også kjøres delvis linjeorientert Din sekvensielle rekursive løsning (som er annotert for PRP) heter MittProg.java 1) Hvis du ønsker det, kjør ditt egent program og notere eksekveringstiden.
>javac MittProg.java >java MittProg 1000000
2) Kompilér PRP-systemet (hvis du ikke har gjort det tidligere) >java javaPRP.java
3) Oversett ditt program (MittProg.java) til et parallelt program (MittProgPara.java) – dette må gjøres via GUI
>java javaPRP – velg da MittProg.java og trykk Compile
4) Kjør det genererte parallelle programmet (MittProgPara.java) > java MittProgPara 1000000
31
PRP kan også kan parallelliseres et fasedelt program
Et fasedelt PRP-program har flere faser som hver består av først en parallell rekursiv del og så en sekvensiell del (kan sløyfes).
Da må brukeren beskrive det i en egen ADMIN –metode:
Innfører da to nye Kommentarkoder: /*ADMIN*/ og /*FUNC 1*/ , /*FUNC 2*/,… osv
32
/*ADMIN*/ public int minAdminMetode(...){ int svar = rekursivMetode1(...); sekvensiellKode1(); svar = rekursivMetode2(...); sekvensiellKode2(...); svar = rekursivMetode3(...); sekvensiellKode3(...); svar = rekursivMetode4(...); sekvensiellKode4(...); return svar; }
Og hver av disse rekursive metodene er i hver sin klasse.
33
class MittFaseProgram{ public static void main(String[] args){ <returverdi> svar = new MittFaseProgram().minAdminMetode(...); } /*ADMIN*/ <returverdi> minAdminMetode(...){ <returverdi> svar1 = new Fase1().rekursivMetode(...); sekvensiellKode1(...); <returverdi> svar2 = new Fase2().rekursivMetode(...); sekvensiellKode2(...); return ...; } void sekvensiellKode1(...){ } void sekvensiellKode2(...){ } } // end MittFaseProgram
class Fase1{ /*FUNC 1*/ <returverdi> rekursivMetode(...){ /*REC*/ <returverdi> svar1 = rekursivMetode(...); /*REC*/ <returverdi> svar2 = rekursivMetode(...); return ...; } } class Fase2{ /*FUNC 2*/ <returverdi> rekursivMetode(...){ /*REC*/ <returverdi> svar1 = rekursivMetode(...); /*REC*/ <returverdi> svar2 = rekursivMetode(...); /*REC*/ <returverdi> svar3 = rekursivMetode(...); return ...; } }
Hvordan gjøres dette? Administrator – arbeider modell
Oppgaver legges ut i et fellesområde Arbeiderne tar oppgaver og legger svar tilbake i
fellesområdet
34
Fra sekvensielt program til parallelt:
35
Figur 5: Fra det sekvensielle programmet, gjennom Java PRP og til det parallelle resultatet. Det nye, parallelle programmet vil inneholde en klasse for main, Admin, Worker pluss eventuelt andre klasser fra det sekvensielle programmet, som ikke er en del av parallelliseringen.
Eksempel: Parallellisering med to tråder
36
Figur 6: Visualisering av treet der vi ønsker å parallellisere med to tråder. Toppen av treet tilsvarer bredde først traversering til vi ender på, i dette tilfelle, to subtrær. Disse to subtrærne vil traverseres dybde først
PRP: Toppen kjøres med tråder bredde-først, så går hver tråd over til dybde først og ‘vanlige’ rekursive kall
Fra bredde først til dybde først og så vanlig rekursjon Trådene blir når de lages lagt i en LenkaListe (FIFO-kø) Så tas den første(A) i køen ut, og den lager to nye barne-tråder (B,C)
som legges i lista. A legges i en stack (LIFO-kø) Neste på i Lista (B) tas ut og dens nye barne-tråder (D,E) legges inn i
Lista B legges på stacken,… osv. Bunnen av bredde-først ligger da på toppen av stacken
37 Figur 7: Administratoren oppretter datastrukturen til arbeiderne.
Figur 8: Datastrukturen etter at Figur 7 er ferdig.
Kjøring og retur av verdier
Når trådene er brukt opp, pop-es stacken (f.eks øverst er E) og vanlige rekursive kall gjøres fra E , neste pop-es …osv. til stacken er tom
Svarene fra ethvert element som tas av stacken legges i en tabell og plukkes opp av den som kalte elementet.
Den som kalte, kan så fortsette sin kode og selv returnere sitt svar,..
38
Figur 9: Arbeiderstacken beregner seg innover til roten.
Svarene genereres nederst stacken og svarene propaganderer oppover til første kall på den rekursive metoden
39
Hvorfor dette med bredde-og dybde-først ?
Kunne vi ikke bare startet trådene og latt de alle gå i parallell? NEI – fordi:
Vi har lovet rekursiv semantikk (virkemåte) i den parallelle. Vi skal derfor oversette det rekursive programmet slik at det gir
alltid samme resultat parallelt.
Eks Quicksort: Anta at vi parallelliserer ned til nivå 3 i treet Hvis nivå 2 og 3 går samtidig, vil dette gå galt fordi de prøver
begge lagene og flytte på de samme elementene i a[].
40
Hva gjøres teknisk – vår program består av (minst) to klasser
Klassen med ‘main’ og en klasse som inneholder den rekursive metoden Er det flere rekursive metoder som skal parallelliseres, så
skal de være inne i hver sin klasse Det genererte programmet består av minst følgende
klasser: Admin Worker
Kort og greit: Det oversatte programmet ‘xxxxxPara.java’ skal vi egentlig
ikke se på og spesielt ikke endre. Det bare virker og parallelliserer etter visse prinsipper.
41
42
Eksempel 2: Største tall i en array
class Search{ int k = 5; /*FUNC*/ int findLargest(int[] arr, int start, int end){ if((end-start) < k){ return largest_baseCase(arr,start,end); } int half = (end-start) / 2; int mid = start + half; /*REC*/ int leftVal = findLargest (arr,start,mid); /*REC*/ int rightVal = findLargest (arr,mid+1,end); if(leftVal > rightVal) return leftVal; return rightVal; } int largest_baseCase(int[] arr, int start, int end){ int largest = 0; for(int i = start; i < end; i++){ if(arr[i] > largest){ largest = arr[i]; } } return largest; } }
class LargestNumber{ public static void main(String[] args){ int len = Integer.parseInt(args[0]); int cores = Runtime.getRuntime().availableProcessors(); int[] arr = new int[len]; Random r = new Random(); for(int i = 0; i < arr.length; i++){ arr[i] = r.nextInt(len-1); } long t = System.nanoTime(); /*CALL*/ int k = (new Search()).findLargest(arr,0,arr.length); Double t2 =(System.nanoTime()-t)/1000000.0; System.out.println("Largest number is " + k+ ", paa:"+t2+"ms."); } }
NB. for å få kjøretiden riktig for det parallelle programmet
N.B det som oppgis som «program execution time» er med overhead fra GUI løsningen. Fra GUI-en:
Kjør det i linjemodus (n= 100 mill.):
43
Opening: LargestNumber.java Created: LargestNumberPara.java javac LargestNumberPara.java java LargestNumberPara 100000000 Largest number is 99999994, paa:152.171677ms. program execution time: 1511.89 ms
M:\INF2440Para\PRP>java LargestNumberPara 100000000 Largest number is 99999998, paa:135.922687ms. M:\INF2440Para\PRP>java LargestNumber 100000000 Largest number is 99999998, paa:417.98199ms.
Hva så vi på i Uke10
Automatisk parallellisering av rekursjon PRP- Parallel Recursive Procedures
Nåværende løsning (Java, multicore CPU, felles hukommelse) –implementasjon: Peter L. Eidsvik
Demo av to eksempler kjøring Hvordan ikke gå i fella: Et Rekursivt kall = En Tråd
Halvautomatisk oversettelse (hint/kommandoer fra kommentarer)
Hvordan virker en kompilator (preprosessor) for automatisk parallellisering Prinsipper ( bredde-først og dybde-først traversering av r-treet) Datastruktur Eksekvering
Krav til et program som skal bruke PRP Slik bruker du PRP – Ukeoppgave neste uke.
44