2010-07-28 19 views
13

Sorprendentemente, el uso de PLINQ no produjo beneficios en un pequeño caso de prueba que creé; de hecho, fue incluso peor de lo habitual LINQ.PLINQ realiza peor de lo habitual LINQ

Aquí está el código de prueba:

int repeatedCount = 10000000; 
    private void button1_Click(object sender, EventArgs e) 
    { 
     var currTime = DateTime.Now; 
     var strList = Enumerable.Repeat(10, repeatedCount); 
     var result = strList.AsParallel().Sum(); 

     var currTime2 = DateTime.Now; 
     textBox1.Text = (currTime2.Ticks-currTime.Ticks).ToString(); 

    } 

    private void button2_Click(object sender, EventArgs e) 
    { 
     var currTime = DateTime.Now; 
     var strList = Enumerable.Repeat(10, repeatedCount); 
     var result = strList.Sum(); 

     var currTime2 = DateTime.Now; 
     textBox2.Text = (currTime2.Ticks - currTime.Ticks).ToString(); 
    } 

El resultado?

textbox1: 3437500 
textbox2: 781250 

Por lo tanto, LINQ toma menos tiempo que PLINQ para completar una operación similar.

¿Qué estoy haciendo mal? ¿O hay un giro que no conozco?

Editar: He actualizado mi código para usar el cronómetro y, sin embargo, el mismo comportamiento persistió. Para descontar el efecto de JIT, lo intenté algunas veces haciendo clic en button1 y button2 y sin ningún orden en particular. Aunque el tiempo que llegué podría ser diferente, pero el comportamiento cualitativo se mantuvo: PLINQ fue de hecho más lento en este caso.

+4

Consejo: Utilice la clase 'Cronómetro' para medir el rendimiento. Será más preciso para medir períodos de tiempo que 'DateTime.Now'. – Greg

+2

@ Anthony Pegram, ¿cómo es eso? Puedo ver fácilmente lo que cada tipo será. – CaffGeek

+0

¿Podría decirnos en qué hardware está ejecutando esto? –

Respuesta

21

Primero: Deje de usar DateTime para medir el tiempo de ejecución. Use un cronómetro en su lugar. El código de prueba se vería así:

var watch = new Stopwatch(); 

var strList = Enumerable.Repeat(10, 10000000); 

watch.Start(); 
var result = strList.Sum(); 
watch.Stop(); 

Console.WriteLine("Linear: {0}", watch.ElapsedMilliseconds); 

watch.Reset(); 

watch.Start(); 
var parallelResult = strList.AsParallel().Sum(); 
watch.Stop(); 

Console.WriteLine("Parallel: {0}", watch.ElapsedMilliseconds); 

Console.ReadKey(); 

Segundo: Correr cosas en paralelo implica una sobrecarga. En este caso, PLINQ tiene que descubrir la mejor manera de dividir su colección para que pueda Sumar los elementos de forma segura en paralelo. Después de eso, debe unir los resultados de los diversos hilos creados y Sumarlos también. Esta no es una tarea trivial.

Usando el código anterior, puedo ver que al usar Sum() se conecta una llamada ~ 95ms. Llamar a .AsParallel(). Suma() redes alrededor de ~ 185 ms.

Realizar una tarea en Paralelo es solo una buena idea si obtienes algo al hacerlo. En este caso, Sum es una tarea bastante simple que no se gana con PLINQ.

+1

+1 @Justin Niessner Respuesta muy nítida. @Ngu Soon Hui PLINQ será útil, pero si no lo entiendes bien, también será malo. Le sugiero que vea Cache False Sharing y GC Throttling antes de comenzar a jugar con PLINQ. –

+0

Al parecer, la incapacidad del sistema para reconocer el hecho de que el uso de PLINQ es más lento contradice la afirmación en esta publicación http://stackoverflow.com/questions/2893637/is-it-ok-to-try-to-use-plinq- in-all-linq-queries que el sistema puede elegir LINQ o PLINQ dependiendo de cuál sea más rápido. – Schultz9999

+0

ejecutó el código en mono, plinq fue ligeramente más rápido – CuiPengFei

1

¿Es posible que no esté teniendo en cuenta el tiempo de JIT? Debe ejecutar su prueba dos veces y descartar el primer conjunto de resultados.

Además, no se debe utilizar para conseguir la sincronización DateTime rendimiento, utilice la clase Stopwatch lugar:

var swatch = new Stopwatch(); 
swatch.StartNew(); 

var strList = Enumerable.Repeat(10, repeatedCount); 
var result = strList.AsParallel().Sum(); 

swatch.Stop(); 
textBox1.Text = swatch.Elapsed; 

PLINQ le añade algo de sobrecarga para el procesamiento de una secuencia. Pero la diferencia de magnitudes en su caso parece excesiva. PLINQ tiene sentido cuando el costo general se ve compensado por el beneficio de ejecutar la lógica en múltiples núcleos/CPU. Si no tiene múltiples núcleos, el procesamiento en paralelo no ofrece ninguna ventaja real, y PLINQ debe detectar dicho caso y realizar el procesamiento secuencialmente.

EDITAR: Al crear pruebas de rendimiento integradas de este tipo, debe asegurarse de que no las ejecuta bajo el depurador o con Intellitrace habilitado, ya que pueden sesgar significativamente los tiempos de ejecución.

0

Eso de hecho puede ser el caso porque está aumentando el número de conmutadores de contexto y no está realizando ninguna acción que se beneficiaría de tener subprocesos esperando algo como la finalización de E/S. Esto será aún peor si se ejecuta en una sola caja de CPU.

0

Recomendaría usar la clase Cronómetro para medir el tiempo. En tu caso, es una mejor medida del intervalo.

0

Lea la sección de Efectos secundarios de este artículo.

http://msdn.microsoft.com/en-us/magazine/cc163329.aspx

Creo que se puede ejecutar en muchas condiciones donde PLINQ tiene patrones de procesamiento de datos adicionales que usted debe entender antes de optar a pensar que es siempre puramente tener tiempos de respuesta más rápidos.

+0

No hay ningún efecto secundario aquí ... –

8

Otros han señalado algunos defectos en sus puntos de referencia. He aquí una aplicación de consola corta para hacerlo más sencillo:

using System; 
using System.Diagnostics; 
using System.Linq; 

public class Test 
{ 
    const int Iterations = 1000000000; 

    static void Main() 
    { 
     // Make sure everything's JITted 
     Time(Sequential, 1); 
     Time(Parallel, 1); 
     Time(Parallel2, 1); 
     // Now run the real tests 
     Time(Sequential, Iterations); 
     Time(Parallel, Iterations); 
     Time(Parallel2, Iterations); 
    } 

    static void Time(Func<int, int> action, int count) 
    { 
     GC.Collect(); 
     Stopwatch sw = Stopwatch.StartNew(); 
     int check = action(count); 
     if (count != check) 
     { 
      Console.WriteLine("Check for {0} failed!", action.Method.Name); 
     } 
     sw.Stop(); 
     Console.WriteLine("Time for {0} with count={1}: {2}ms", 
          action.Method.Name, count, 
          (long) sw.ElapsedMilliseconds); 
    } 

    static int Sequential(int count) 
    { 
     var strList = Enumerable.Repeat(1, count); 
     return strList.Sum(); 
    } 

    static int Parallel(int count) 
    { 
     var strList = Enumerable.Repeat(1, count); 
     return strList.AsParallel().Sum(); 
    } 

    static int Parallel2(int count) 
    { 
     var strList = ParallelEnumerable.Repeat(1, count); 
     return strList.Sum(); 
    } 
} 

Compilación:

csc /o+ /debug- Test.cs 

Resultados en mi quad core i7 portátil; ejecuta hasta 2 núcleos rápido o 4 núcleos más lentamente. Básicamente gana ParallelEnumerable.Repeat, seguido de la versión de secuencia, seguida de la paralelización de Enumerable.Repeat normal.

Time for Sequential with count=1: 117ms 
Time for Parallel with count=1: 181ms 
Time for Parallel2 with count=1: 12ms 
Time for Sequential with count=1000000000: 9152ms 
Time for Parallel with count=1000000000: 44144ms 
Time for Parallel2 with count=1000000000: 3154ms 

Tenga en cuenta que las versiones anteriores de esta respuesta fueron vergonzosamente viciados por tener un número incorrecto de elementos - Soy mucho más confianza en los resultados anteriores.

+0

No veo ninguna diferencia fundamental entre su punto de referencia y el mío, pero ¿por qué nuestros resultados son sustancialmente diferentes? – Graviton

+1

@Ngu: ¿Está ejecutando su código bajo el depurador o utilizando la función Intellitrace de VS2010? Esos pueden sesgar dramáticamente los resultados de los tiempos de ejecución. Lo ideal es intentar ejecutar una compilación de lanzamiento en la línea de comandos, no desde VS. – LBushkin

+1

@Ngu: 1) El mío no se ejecuta en el contexto de una IU. No sé si eso hace alguna diferencia, pero ¿por qué dar más trabajo al benchmark? 2) El mío claramente elimina el JIT de la ecuación. 3) El mío le da a las tareas más trabajo por hacer (por un factor de 100). La diferencia entre el cronómetro y la fecha y hora es un * bit * de arenga roja, IMO: para el momento en que haya períodos de tiempo razonablemente largos (varios segundos), la imprecisión de DateTime será insignificante. –

0

El comentario de Justin sobre la sobrecarga es exactamente correcto.

Sólo es algo a tener en cuenta al escribir el software concurrente en general, más allá del uso de PLINQ:

siempre hay que pensar en la "granularidad" de sus elementos de trabajo. Algunos problemas son muy adecuados para la paralelización, ya que se pueden "fragmentar" en un nivel muy alto, como el raytracing marcos enteros al mismo tiempo (este tipo de problemas se denominan embarazosamente paralelos). Cuando hay "trozos" de trabajo muy grandes, la sobrecarga de crear y administrar varios subprocesos se vuelve insignificante en comparación con el trabajo real que desea realizar.

PLINQ hace que la programación simultánea sea más fácil, pero eso no significa que pueda ignorar la granularidad de su trabajo.

22

Este es un error clásico: pensar: "Ejecutaré una prueba simple para comparar el rendimiento de este código de subproceso único con este código multiproceso".

Un simple análisis de es el tipo peor de prueba que puede funcionar para medir el rendimiento multi-hilo.

Normalmente, la paralelización de algunas operaciones produce un beneficio de rendimiento cuando los pasos que está paralelizando requieren un trabajo sustancial. Cuando los pasos son simples, como en, rápido *, la sobrecarga de paralelizar su trabajo termina empequeñeciendo la minúscula ganancia de rendimiento que de otra manera hubiera obtenido.


Considere esta analogía.

Estás construyendo un edificio.Si tiene un trabajador, debe tender ladrillos uno por uno hasta que forme una pared, luego haga lo mismo para la siguiente pared, y así sucesivamente hasta que todas las paredes estén construidas y conectadas. Esta es una tarea lenta y laboriosa que podría beneficiarse de la paralelización.

El derecho manera de hacer esto sería para paralelizar el edificio pared - contratar, digamos, 3 trabajadores más, y hacer que cada trabajador de la construcción de su propia pared de modo que 4 paredes se pueden construir de forma simultánea. El tiempo que lleva encontrar a los 3 trabajadores adicionales y asignarles sus tareas es insignificante en comparación con los ahorros que obtienes al obtener 4 paredes en la cantidad de tiempo que le hubiera llevado construir 1.

El mal forma de hacerlo sería paralelizar el colocación de ladrillos - contratar alrededor de mil trabajadores más y hacer que cada trabajador sea responsable de colocar un solo ladrillo a la vez. Usted puede pensar: "Si un trabajador puede poner 2 ladrillos por minuto, entonces mil trabajadores deberían poder colocar 2000 ladrillos por minuto, así que terminaré este trabajo en muy poco tiempo". Pero la realidad es que paralelizando su carga de trabajo a un nivel tan microscópico, está desperdiciando una gran cantidad de energía reuniendo y coordinando a todos sus trabajadores, asignándoles tareas ("coloque este ladrillo allí mismo"), asegurándose de que nadie el trabajo está interfiriendo con cualquier otra persona, etc.

Así que la moraleja de esta analogía es: en general, el uso de paralelización dividir la unidades sustanciales de trabajo (como paredes), pero deje las insustanciales unidades (como ladrillos) para ser manejado de la manera habitual secuencial.


* Por esta razón, en realidad se puede hacer una aproximación bastante buena de la ganancia de rendimiento de paralelización en un contexto más intensivas en trabajo mediante la adopción de cualquier código de rápida ejecución y la adición de Thread.Sleep(100) (o algún otro número al azar) hasta el final. De repente, las ejecuciones secuenciales de este código se ralentizarán en 100 ms por iteración, mientras que las ejecuciones paralelas se ralentizarán significativamente menos.

1

Algo más importante que no he visto mencionado es que .AsParallel tendrá un rendimiento diferente en función de la colección utilizada.

En mis pruebas PLINQ es más rápido de LINQ cuando no se utiliza en IEnumerable (Enumerable.Repeat):

29ms PLINQ ParralelQuery  
    30ms LINQ ParralelQuery  
    30ms PLINQ Array 
    38ms PLINQ List  
163ms LINQ IEnumerable 
211ms LINQ Array 
213ms LINQ List 
273ms PLINQ IEnumerable 
4 processors 

Código es en VB, pero a fin de mostrar que el uso de .ToArray hizo la versión PLINQ par de veces más rápido

Dim test = Function(LINQ As Action, PLINQ As Action, type As String) 
        Dim sw1 = Stopwatch.StartNew : LINQ() : Dim ts1 = sw1.ElapsedMilliseconds 
        Dim sw2 = Stopwatch.StartNew : PLINQ() : Dim ts2 = sw2.ElapsedMilliseconds 
        Return {String.Format("{0,4}ms LINQ {1}", ts1, type), String.Format("{0,4}ms PLINQ {1}", ts2, type)} 
       End Function 

    Dim results = New List(Of String) From {Environment.ProcessorCount & " processors"} 
    Dim count = 12345678, iList = Enumerable.Repeat(1, count) 

    With iList : results.AddRange(test(Sub() .Sum(), Sub() .AsParallel.Sum(), "IEnumerable")) : End With 
    With iList.ToArray : results.AddRange(test(Sub() .Sum(), Sub() .AsParallel.Sum(), "Array")) : End With 
    With iList.ToList : results.AddRange(test(Sub() .Sum(), Sub() .AsParallel.Sum(), "List")) : End With 
    With ParallelEnumerable.Repeat(1, count) : results.AddRange(test(Sub() .Sum(), Sub() .AsParallel.Sum(), "ParralelQuery")) : End With 

    MessageBox.Show(String.join(Environment.NewLine, From l In results Order By l)) 

Ejecución de las pruebas en diferente orden tendrá diferentes resultados un poco, así tenerlos en una línea les hace moverse arriba y abajo un poco más fácil para mí.

Cuestiones relacionadas