2011-11-02 35 views
9

Editar: Mi tamaño de muestra es demasiado pequeño. Cuando lo ejecuté contra los datos reales en 8 CPUs, vi un aumento de velocidad de 7.2x. No está mal para agregar 4 caracteres a mi código;)Tiempo de ejecución de la colección paralela Scala

Actualmente estoy en el proceso de tratar de "vender" la gestión de los beneficios de usar Scala, especialmente cuando se trata de escalar con CPU. Con ese fin, creé una aplicación de prueba simple que hace un montón de matemáticas vectoriales y me sorprendió un poco descubrir que el tiempo de ejecución no era notablemente mejor en mi máquina de cuatro núcleos. Curiosamente, descubrí que el tiempo de ejecución es el peor la primera vez que revisas la colección y mejora con las llamadas posteriores. ¿Hay algunas cosas perezosas en la colección paralela que están causando esto, o lo estoy haciendo mal? Cabe señalar que vengo del mundo C++/C#, por lo que es muy posible que haya estropeado mi configuración de alguna manera. De todos modos, aquí está mi configuración:

InteliJ Scala Plugin

Scala 2.9.1.final

Windows 7 64 bits, procesador de cuatro núcleos (sin 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") 
    } 
    } 
} 

La salida de este programa es:

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 

¿Qué está pasando aquí? Las primeras 2 veces que hacemos el filtro, es más lento, y luego las cosas se aceleran? Entiendo que habrá un costo de inicio de paralelismo intrínsecamente, solo estoy tratando de descubrir dónde tiene sentido expresar el paralelismo en mi aplicación, y específicamente quiero poder mostrar a la Administración un programa que se ejecuta 3-4 veces más rápido en una caja de cuatro núcleos. ¿Esto no es un buen problema?

Ideas?

+2

Si está buscando alguna idea sobre cómo vender administración, puede echarle un vistazo a http://scala-boss.heroku.com/#1 (use las teclas de flecha para ver las siguientes diapositivas). – huynhjl

+1

En general, prefieren matrices paralelas a vectores paralelos, al menos hasta que se agreguen concates a vectores. – axel22

+0

@huynhjl - Sabía que la presentación valía algo cuando vi mi vida representada en los primeros dos cómics. ¡Gracias! – fbl

Respuesta

11

Tiene la enfermedad de micro-referencia. Lo más probable es que compares la fase de compilación JIT. Necesitarás calentar primero tu JIT con una prueba previa.

Probablemente la mejor idea es utilizar un marco de micro-benchmarking como http://code.google.com/p/caliper/ que maneja todo eso para usted.

Editar: Hay un bonito SBT Template para proyectos Calibre evaluación comparativa Scala, como se indica from this blog post

0

¿qué tal

val numTargetsInRange_sequential = parTargets.filter(within50) 

?

Además, es probable que obtenga resultados más impresionantes con un mapa en lugar de una operación de filtro.

7

las cosas acelerar, pero no tiene nada que ver con el paralelo frente secuencial, no se está comparando manzanas con manzanas. La JVM tiene un compilador JIT (justo a tiempo) que compilará un código de bytes solo después de usar el código una cierta cantidad de veces. Entonces, lo que ves en las primeras iteraciones es una ejecución más lenta para el código que aún no está JIT-ed, así como el tiempo para la compilación de JIT en curso.Extracción de la .par por lo que todo es secuencial aquí es lo que veo en mi máquina (10x menos iteración porque estoy usando una máquina antigua):

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 

Pero todo es secuencial! Puede ver lo que hace la JVM en términos de JIT usando JVM -XX:+PrintCompilation (establecer en JAVA_OPTS o usar la opción de escala -J-XX:+PrintCompilation. En las primeras iteraciones verá un gran número de instrucciones de impresión de JVM que muestran lo que está siendo JIT-ed, luego se estabiliza .. más adelante

Así que para comparar manzanas con manzanas, ejecuta por primera vez sin la par, a continuación, añadir el par y ejecutar el mismo programa en mi doble núcleo, cuando se utiliza .par me sale:

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 

Así más o menos una aceleración de 2x una vez que es estable.

En relación nota ted, la otra cosa con la que debes tener cuidado es boxear y des-boxear, especialmente si estás comparando con solo Java. Las funciones de alto orden de la biblioteca scala como filtro están haciendo boxeo y desacoplando tipos primitivos, y esto suele ser una fuente de decepción inicial para aquellos que convierten código de Java a Scala.

A pesar de que no se aplica en este caso como el for está fuera del tiempo, hay también un cierto costo para el uso de for en lugar de while, pero el compilador 2.9.1 debería estar haciendo un buen trabajo al usar la bandera -optimize scalac .

3

Junto a las optimizaciones JIT mencionadas anteriormente, un concepto clave que debe evaluar es si su problema se basa en la paralelización: existe un costo inherente de división, coordinación de subprocesos que une la ventaja de hacer cosas en paralelo. Scala oculta esta complejidad, pero necesita saber cuándo aplicarla para obtener buenos resultados.

En su caso, aunque está realizando una gran cantidad de operaciones, cada operación en sí misma es casi trivial en la CPU. Para ver las colecciones paralelas en acción, intente una operación que sea pesada en una unidad.

Para una presentación similar Scala, he usado un algo sencillo (innefficient) para calcular si un número es primo: def isPrime(x:Int) = (2 to x/2).forall(y=>x%y!=0)

continuación, utilizar la misma lógica que presentaste para determinar los números de una colección que son primos :

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

el comportamiento de la CPU realmente mostró la diferencia entre ambos: prime numbers: sequential vs parallel

el tiempo estaba a punto Bette 3.5x r para las colecciones paralelas en una CPU de 4 núcleos.

Cuestiones relacionadas