2010-09-02 13 views
16

estoy ejecutando el código Scala en un sistema de cuatro núcleos Core 2 de 32 bits:¿Por qué mis futuros scala no son más eficientes?

def job(i:Int,s:Int):Long = { 
    val r=(i to 500000000 by s).map(_.toLong).foldLeft(0L)(_+_) 
    println("Job "+i+" done") 
    r 
} 

import scala.actors.Future 
import scala.actors.Futures._ 

val JOBS=4 

val jobs=(0 until JOBS).toList.map(i=>future {job(i,JOBS)}) 
println("Running...") 
val results=jobs.map(f=>f()) 
println(results.foldLeft(0L)(_+_)) 

(Sí, lo hago sabemos que hay mucho formas más eficientes para sumar una serie de números enteros; es solo para darle algo al CPU).

Dependiendo de lo que se propuso a JOBS, el código se ejecuta en los siguientes momentos:

JOBS=1 : 31.99user 0.84system 0:28.87elapsed 113%CPU 
JOBS=2 : 27.71user 1.12system 0:14.74elapsed 195%CPU 
JOBS=3 : 33.19user 0.39system 0:13.02elapsed 257%CPU 
JOBS=4 : 49.08user 8.46system 0:22.71elapsed 253%CPU 

me sorprende que esto realmente no escala bien más allá de 2 de futuros "en juego". Hago un montón de código C++ multiproceso y no tengo dudas de que obtendría una buena escala de hasta 4 núcleos y vería> 390% de utilización de CPU si codificara este tipo de cosas con TBB de Intel o boost::threads (sería mucho más prolijo de curso).

Entonces, ¿qué está pasando y cómo puedo obtener la escala de 4 núcleos que esperaría ver? ¿Está esto limitado por algo en Scala o JVM? Se me ocurre que realmente no sé "dónde" se ejecuta el futuro de scala ... es un hilo creado en el futuro, o "Futures" proporciona un grupo de hilos dedicado a ejecutarlos?

[Estoy usando la Scala 2.7.7 paquetes de Debian/Squeeze en un sistema de Lenny con el sol-java6 (6-20-0lennny1).]

Actualización:

Como se sugirió en la respuesta de Rex, grabé para evitar la creación de objetos.

def job(i:Long,s:Long):Long = { 
    var t=0L 
    var v=i 
    while (v<=10000000000L) { 
    t+=v 
    v+=s 
    } 
    println("Job "+i+" done") 
    t 
} 
// Rest as above... 

¡Esto fue mucho más rápido tuve que aumentar significativamente el recuento de iteraciones para ejecutar durante cualquier cantidad de tiempo! Los resultados son:

JOBS=1: 28.39user 0.06system 0:29.25elapsed 97%CPU 
JOBS=2: 28.46user 0.04system 0:14.95elapsed 190%CPU 
JOBS=3: 24.66user 0.06system 0:10.26elapsed 240%CPU 
JOBS=4: 28.32user 0.12system 0:07.85elapsed 362%CPU 

que es mucho más parecido a lo que yo espero ver (aunque el caso 3 puestos de trabajo es un poco extraño, con la realización de una tarea consistente un par de segundos antes de que los otros dos).

Empujar un poco más allá, en un hyperthreaded de cuatro núcleos i7 esta última versión con JOBS=8 logra un aumento de velocidad X4.4 vs JOBS = 1, con el uso de CPU 571%.

+1

¡Estás impaciente, deseando el futuro hoy! En serio, Rex dio en el clavo, estás evaluando la recolección de basura, no la eficiencia de los futuros. –

+1

Heh ... muy cierto.Cuando envié esta pregunta, no había estado usando Scala durante tanto tiempo, y probablemente era demasiado crédulo con respecto a la extrema exageración que la rodeaba. – timday

+0

cuídate de volver a ejecutar la prueba con akka.dispatch.Future? –

Respuesta

14

Supongo que el recolector de basura está haciendo más trabajo que la propia adición. Por lo tanto, está limitado por lo que el recolector de basura puede administrar. Intente ejecutar la prueba de nuevo con algo que no cree ningún objeto (por ejemplo, use un ciclo while en lugar del rango/mapa/pliegue). También puede jugar con las opciones paralelas de GC si su aplicación real afectará al GC de esta manera.

+0

Sí, ese parece ser el caso; ver la segunda versión del código y los resultados en la actualización de la pregunta. El problema originalmente surgió en algunos códigos haciendo un uso intensivo de BigInts, por lo que no hay muchas posibilidades de eliminar la creación de objetos allí. No había apreciado cuánto impacto podría tener este tipo de cosas ... scala parece eliminar la necesidad de muchas novedades explícitas en el código, por lo que es fácil olvidar que todavía está allí. – timday

+0

¿No debería el compilador optimizar este 'nuevo'? –

+2

@Elazar - Eventualmente con especialización, _may_ será posible que eso (o algo similar) se ejecute sin creación de objetos. Por ahora, sin embargo, es inevitable: el código es genérico, por lo que debe crear el objeto para que funcione, incluso si es solo un contenedor sobre un primitivo. –

2

Trate

(i to 500000000 by s).view.map(_.toLong).foldLeft(0L)(_+_) 

La aplicación de view se supone que (como se entendía id) para evitar iteración repetida y la creación de objetos proporcionando envolturas simples.

Tenga en cuenta también que puede usar reduceLeft(_+_) en lugar de doblar.

+0

Todavía estoy en 2.7.7; view no es miembro de Range for me (con suerte 2.8 aparecerá en el archivo de Debian un día; soy perezoso para compilar desde el origen, y todos los libros de Scala que tengo son de aproximadamente 2.7). Me alegra ver que se están haciendo algunas mejoras en esta área. (Y sí, generalmente preferiría reducir a doblar a menos que haya alguna buena razón, en este caso fue porque no hay garantía de que algunos parámetros de i/s extremos para la función no conduzcan a uno o cero elementos para la operación de reducción). – timday

+0

Sí, lo noté también. Puede descargar Scala 2.8.1 en una tgz que solo tiene que desempaquetar en cualquier lugar (por ejemplo, '/ usr/share /'). Luego crea enlaces simbólicos a los scripts en la subcarpeta 'bin' y eres oro. Realmente me interesaría cómo funcionan las tres variantes en 2.8.1 en tu máquina (no tengo un quad-core). – Raphael

Cuestiones relacionadas