2011-11-02 11 views
9

Edytuj: Mój rozmiar próbki był zbyt mały. Kiedy uruchomiłem go na podstawie rzeczywistych danych na 8 procesorach, widziałem wzrost prędkości 7,2x. Nie jest zbyt obskurny, aby dodać 4 znaki do mojego kodu;)Scala kolekcja równoległa runtime zagadkowe

Obecnie jestem w trakcie próby "sprzedaży" zarządzania na korzyściach płynących z używania Scali, szczególnie jeśli chodzi o skalowanie z procesorami. W tym celu stworzyłem prostą aplikację testową, która wykonuje garść matematyki wektorowej i był nieco zaskoczony tym, że środowisko wykonawcze nie było wyraźnie lepsze na mojej czterordzeniowej maszynie. Co ciekawe, stwierdziłem, że środowisko wykonawcze jest najgorsze za pierwszym razem, gdy przechodzisz przez kolekcję i poprawia się z kolejnymi połączeniami. Czy są jakieś leniwe rzeczy w kolekcji równoległej, które to powodują, czy też robię to źle? Należy zauważyć, że pochodzę ze świata C++/C#, więc jest całkiem możliwe, że jakoś zawiodłem swoją konfigurację. Niezależnie od tego, oto moja konfiguracja:

InteliJ Scala Plugin

Scala 2.9.1.final

Windows 7 64 bit, procesor Quad-Core (brak hyperthreading)

import util.Random 

    // simple Vector3D class that has final x,y,z components a length, and a '-' function 
    class Vector3D(val x:Double, val y:Double, val z:Double) 
    { 
    def length = math.sqrt(x*x+y*y+z*z) 
    def -(rhs : Vector3D) = new Vector3D(x - rhs.x, y - rhs.y, z - rhs.z) 
    } 

object MainClass { 

    def main(args : Array[String]) = 
    { 
    println("Available CPU's: " + Runtime.getRuntime.availableProcessors()) 
    println("Parallelism Degree set to: " + collection.parallel.ForkJoinTasks.defaultForkJoinPool.getParallelism); 
    // my position 
    val myPos = new Vector3D(0,0,0); 

    val r = new Random(0); 

    // define a function nextRand that gets us a random between 0 and 100 
    def nextRand = r.nextDouble() * 100; 

    // make 10 million random targets 
    val targets = (0 until 10000000).map(_ => new Vector3D(nextRand, nextRand, nextRand)).toArray 
    // take the .par hit before we start profiling 
    val parTargets = targets.par 

    println("Created " + targets.length + " vectors") 

    // define a range function 
    val rangeFunc : (Vector3D => Double) = (targetPos) => (targetPos - myPos).length 

    // we'll select ones that are <50 
    val within50 : (Vector3D => Boolean) = (targetPos) => rangeFunc(targetPos) < 50 

    // time it sequentially 
    val startTime_sequential = System.currentTimeMillis() 
    val numTargetsInRange_sequential = targets.filter(within50) 
    val endTime_sequential = System.currentTimeMillis() 
    println("Sequential (ms): " + (endTime_sequential - startTime_sequential)) 

    // do the parallel version 10 times 
    for(i <- 1 to 10) 
    { 

     val startTime_par = System.currentTimeMillis() 
     val numTargetsInRange_parallel = parTargets.filter(within50) 
     val endTime_par = System.currentTimeMillis() 

     val ms = endTime_par - startTime_par; 
     println("Iteration[" + i + "] Executed in " + ms + " ms") 
    } 
    } 
} 

Wyjście ten program to:

Available CPU's: 4 
Parallelism Degree set to: 4 
Created 10000000 vectors 
Sequential (ms): 216 
Iteration[1] Executed in 227 ms 
Iteration[2] Executed in 253 ms 
Iteration[3] Executed in 76 ms 
Iteration[4] Executed in 78 ms 
Iteration[5] Executed in 77 ms 
Iteration[6] Executed in 80 ms 
Iteration[7] Executed in 78 ms 
Iteration[8] Executed in 78 ms 
Iteration[9] Executed in 79 ms 
Iteration[10] Executed in 82 ms 

Co tu się dzieje? Pierwsze 2 razy, kiedy robimy filtr, jest wolniejsze, a potem przyspieszenie? Rozumiem, że z natury będzie to koszt uruchomienia równoległego, próbuję po prostu dowiedzieć się, gdzie ma sens wyrażenie paralelizmu w mojej aplikacji, a konkretnie chcę być w stanie pokazać zarządowi program, który działa 3-4 razy szybciej na pudełku z rdzeniem Quad. Czy to nie jest dobry problem?

Pomysły?

+2

Jeśli szukasz pomysłu na zarządzanie sprzedażą, zajrzyj na http://scala-boss.heroku.com/#1 (użyj strzałek, aby zobaczyć kolejne slajdy). – huynhjl

+1

Generalnie preferuj równoległe tablice do równoległych wektorów, przynajmniej do dodania konkatów do wektorów. – axel22

+0

@huynhjl - Wiedziałem, że prezentacja jest coś warta, gdy zobaczyłem moje życie przedstawione w dwóch pierwszych komiksach. Dzięki! – fbl

Odpowiedz

11

Masz mikro-punkt odniesienia choroby. Najprawdopodobniej przeprowadzasz analizę porównawczą fazy kompilacji JIT. Najpierw musisz rozgrzać swój JIT w trybie pre-run.

Prawdopodobnie najlepszym pomysłem jest wykorzystanie architektury mikro-benchmarkingowej, takiej jak http://code.google.com/p/caliper/, która obsługuje to wszystko za Ciebie.

Edit: Jest miły SBT Template projektów Suwmiarka benchmarking Scala, jak podano from this blog post

0

jak o

val numTargetsInRange_sequential = parTargets.filter(within50) 

?

Prawdopodobnie uzyskasz bardziej imponujące wyniki dzięki mapie, a nie operacji filtrowania.

7

rzeczy się przyspieszyć, ale to nie ma nic wspólnego z równoległej kontra sekwencyjnego, nie jesteś porównywanie jabłek do jabłek. JVM ma kompilator JIT (just in time), który skompiluje kod bajtowy tylko po tym, jak kod zostanie użyty określoną liczbę razy. W pierwszych iteracjach widać wolniejsze wykonywanie kodu, który nie jest jeszcze JIT-em, a także czas na ciągłą kompilację JIT.Zdejmowanie .par tak, że to wszystko sekwencyjną oto co widzę na moim komputerze (10x mniej iteracji przyczyna Używam starszą maszynę):

Sequential (ms): 312 
Iteration[1] Executed in 117 ms 
Iteration[2] Executed in 112 ms 
Iteration[3] Executed in 112 ms 
Iteration[4] Executed in 112 ms 
Iteration[5] Executed in 114 ms 
Iteration[6] Executed in 113 ms 
Iteration[7] Executed in 113 ms 
Iteration[8] Executed in 117 ms 
Iteration[9] Executed in 113 ms 
Iteration[10] Executed in 111 ms 

Ale to wszystko sekwencyjny! Możesz zobaczyć, co robi JVM pod względem JIT, używając JVM -XX:+PrintCompilation (ustaw w JAVA_OPTS lub użyj opcji scala -J-XX:+PrintCompilation. W pierwszych iteracjach zobaczysz dużą liczbę instrukcji drukowania JVM pokazujących, co jest JIT-ed, a następnie stabilizuje .. później

więc porównać jabłka do jabłek, najpierw uruchomić bez par, a następnie dodać par i uruchomić ten sam program na moim dwurdzeniowe, gdy przy użyciu .par uzyskać:

Sequential (ms): 329 
Iteration[1] Executed in 197 ms 
Iteration[2] Executed in 60 ms 
Iteration[3] Executed in 57 ms 
Iteration[4] Executed in 58 ms 
Iteration[5] Executed in 59 ms 
Iteration[6] Executed in 73 ms 
Iteration[7] Executed in 56 ms 
Iteration[8] Executed in 60 ms 
Iteration[9] Executed in 58 ms 
Iteration[10] Executed in 57 ms 

Tak mniej więcej 2x przyspieszenie, gdy jest stabilny.

Na rela Uwaga, drugą rzeczą, którą chcesz zachować ostrożność jest boksowanie i un-boxing, szczególnie jeśli porównujesz tylko do Javy. Funkcje wysokiej klasy biblioteki scala, takie jak filtr, wykonują boksowanie i un-boxing typów pierwotnych i jest to zazwyczaj źródło początkowego rozczarowania dla tych, którzy konwertują kod z Java na Scalę.

Choć nie ma zastosowania w tym przypadku, jak for jest poza czasu, istnieje również pewne koszty za korzystanie for zamiast while, ale 2.9.1 kompilator powinien robić przyzwoitą pracę przy użyciu flagi -optimize scalac .

3

Obok wspomnianych wcześniej optymalizacji JIT kluczową koncepcją, którą należy ocenić, jest to, czy problem polega na równoległości: nieodłączny koszt podziału, koordynacja wątków i łączenie tych wag z korzyścią równoległego działania. Scala ukrywa przed Tobą tę złożoność, ale musisz wiedzieć, kiedy ją zastosować, aby uzyskać dobre wyniki.

W twoim przypadku, mimo że wykonujesz ogromną liczbę operacji, każda operacja sama w sobie jest prawie nieważna. Aby zobaczyć równoległe kolekcje w akcji, spróbuj operacji, która jest ciężka w jednostce.

Na podobnej prezentacji Scala, użyłem prostego (innefficient) algo do obliczenia, czy dana liczba jest liczbą pierwszą: def isPrime(x:Int) = (2 to x/2).forall(y=>x%y!=0)

Następnie użyć tej samej logiki ty przedstawiane w celu ustalenia liczby w kolekcji, które są podstawowym :

val col = 1 to 1000000 
col.filter(isPrime(_)) // sequential 
col.par.filter(isPrime(_)) // parallel 

zachowanie CPU naprawdę pokazał różnicę między obu: prime numbers: sequential vs parallel

czas był o 3,5x Bette r dla równoległych kolekcji w czterordzeniowym procesorze.