2010-11-13 11 views

Respuesta

54

No, no tiene sentido para todos los foreach. Algunas de las razones:

  • Su código realidad no pueden ser paralelizable. Por ejemplo, si está utilizando los "resultados hasta ahora" para la siguiente iteración y el orden es importante)
  • Si está agregando (por ejemplo, sumando valores), entonces hay formas de usar Parallel.ForEach para esto, pero no debería 't apenas lo hacen ciegamente
  • Si su trabajo se completará muy rápido de todos modos, no hay beneficio, y bien puede cosas lentas abajo

Básicamente nada en el roscado que debe hacerse a ciegas. Piense dónde realmente hace sentido para paralelizar. Ah, y mida el impacto para asegurarse de que el beneficio valga la complejidad añadida. (Es será más difícil para cosas como la depuración.) TPL es genial, pero no es un almuerzo gratis.

1

No debe reemplazar ciegamente cada bucle foreach en su aplicación con el foreach paralelo. Más hilos no significa necesariamente que su aplicación funcionará más rápido. Debe dividir la tarea en tareas más pequeñas que podrían ejecutarse en paralelo si realmente desea beneficiarse de múltiples subprocesos. Si su algoritmo no es paralelizable, no obtendrá ningún beneficio.

8

No hay un límite inferior para realizar operaciones paralelas. Si solo tiene 2 elementos para trabajar, pero cada uno tardará un tiempo, podría tener sentido usar Parallel.ForEach. Por otro lado, si tiene 1000000 elementos pero no hacen mucho, el bucle paralelo podría no ir más rápido que el bucle normal.

Por ejemplo, escribí un programa simple para sincronizar el tiempo en bucles donde el bucle externo funcionó tanto con un bucle for como con Parallel.ForEach. Lo sincronicé en mi laptop de 4 CPU (dual-core, hyperthreaded).

Aquí es una carrera con sólo 2 elementos para trabajar, pero cada uno toma un tiempo:

 
2 outer iterations, 100000000 inner iterations: 
for loop: 00:00:00.1460441 
ForEach : 00:00:00.0842240 

Aquí es una carrera con millones de artículos a trabajar, pero no hacen mucho:

 
100000000 outer iterations, 2 inner iterations: 
for loop: 00:00:00.0866330 
ForEach : 00:00:02.1303315 

La única forma real de saber es intentarlo.

+0

@Anthony: No dude en sugerir un número correcto. – Gabe

+0

Parallel.ForEach está diseñado para "1000000 ítems pero no hacen mucho", y donde cada núcleo de CPU puede trabajar en lotes de 10000 a la vez sin interdependencias. – Anthony

+1

@Anthony: vea mi edición para una situación donde 2 operaciones largas son más rápidas con 'Parallel.ForEach' pero 100000000 operaciones triviales son más rápidas con' for'. – Gabe

0

No. Necesita entender qué está haciendo el código y si puede ser paralelo. Las dependencias entre sus elementos de datos pueden dificultar la paralelización, es decir, si un subproceso usa el valor calculado para el elemento anterior, debe esperar hasta que el valor se calcule de todos modos y no pueda ejecutarse en paralelo. También debe comprender su arquitectura de destino, aunque normalmente tendrá una CPU multinúcleo en casi todo lo que compre en estos días. Incluso en un solo núcleo, puede obtener algunos beneficios de más hilos, pero solo si tiene algunas tareas de bloqueo. También debe tener en cuenta que hay una sobrecarga en la creación y organización de los hilos paralelos. Si esta sobrecarga es una fracción significativa de (o más de) el tiempo que le lleva su tarea, puede reducir la velocidad.

17

No, definitivamente no deberías hacer eso. El punto importante aquí no es realmente el número de iteraciones, sino el trabajo por hacer. Si su trabajo es realmente simple, ejecutar 1000000 delegados en paralelo agregará una sobrecarga enorme y probablemente sea más lento que una solución tradicional de un solo hilo. Puede evitar esto dividiendo los datos, de modo que ejecuta trozos de trabajo.

E.g. Consideremos a continuación la situación:

Input = Enumerable.Range(1, Count).ToArray(); 
Result = new double[Count]; 

Parallel.ForEach(Input, (value, loopState, index) => { Result[index] = value*Math.PI; }); 

La operación aquí es tan simple, que la sobrecarga de hacer esto de forma paralela superará con creces la ganancia de utilizar múltiples núcleos. Este código se ejecuta significativamente más lento que un bucle foreach regular.

Al usar una partición, podemos reducir la sobrecarga y, de hecho, observar una ganancia en el rendimiento.

Parallel.ForEach(Partitioner.Create(0, Input.Length), range => { 
    for (var index = range.Item1; index < range.Item2; index++) { 
     Result[index] = Input[index]*Math.PI; 
    } 
}); 

La moral de la historia aquí es que el paralelismo es difícil y sólo se debe emplear este después de mirar de cerca la situación en cuestión. Además, debe perfilar el código antes y después de agregar el paralelismo.

Recuerde que, independientemente de cualquier ganancia potencial, el paralelismo siempre agrega complejidad al código, por lo que si el rendimiento ya es lo suficientemente bueno, hay pocas razones para agregar la complejidad.

+6

"Si su trabajo es realmente simple, la ejecución de 1000000 delegados en paralelo agregará una gran sobrecarga y lo más probable es que sea más lenta que una solución tradicional de un solo subproceso". Esto es incorrecto, y publicaré puntos de referencia que muestren esto. Existe un concepto de "máximo grado de paralelismo" y Parallel.ForEach intentará y decidirá lo que debería ser. No va a intentar "ejecutar 1000000 delegados en paralelo". No siempre tomará la mejor decisión, pero casi siempre tendrá un mejor rendimiento que la no roscada. El particionamiento es una buena idea, y puede establecer explícitamente el MaxDegreeOfParallelism. – AaronLS

1

En general, una vez que se supera un hilo por núcleo, cada hilo adicional involucrado en una operación lo hará más lento, no más rápido.

Sin embargo, si parte de cada operación se bloquea (el ejemplo clásico está esperando en el disco o en la red E/S, otros son productores y consumidores que no están sincronizados) entonces más hilos que núcleos pueden comenzar a acelerar cosas de nuevo, porque las tareas se pueden hacer mientras que otros hilos no pueden avanzar hasta que la operación de E/S regrese. Por esta razón, cuando las máquinas single-core eran la norma, las únicas justificaciones reales en multi-threading eran cuando había bloqueo del tipo I/O introduce o bien mejoraba la capacidad de respuesta (un poco más lento para realizar una tarea). , pero mucho más rápido para comenzar a responder a la entrada del usuario de nuevo).

Aún así, en la actualidad las máquinas de un solo núcleo son cada vez más raras, por lo que parece que debería ser capaz de hacer todo al menos dos veces más rápido con el procesamiento paralelo.

Esto no será el caso si el orden es importante o algo inherente a la tarea lo obliga a tener un cuello de botella sincronizado o si el número de operaciones es tan pequeño que el aumento de la velocidad del procesamiento paralelo es superado por los gastos generales involucrados en la configuración de ese procesamiento paralelo. Puede o no ser el caso si un recurso compartido requiere hilos para bloquear en otros hilos que realizan la misma operación paralela (dependiendo del grado de contención de bloqueo).

Además, si para empezar su código es intrínsecamente multiproceso, puede encontrarse en una situación en la que esencialmente está compitiendo por recursos consigo mismo (un caso clásico es que el código ASP.NET maneja solicitudes simultáneas). Aquí la ventaja de la operación en paralelo puede significar que una sola operación de prueba en una máquina de 4 núcleos se aproxima a 4 veces el rendimiento, pero una vez que el número de solicitudes que necesitan realizar la misma tarea llega a 4, cada una de esas 4 solicitudes es tratando de usar cada núcleo, se vuelve un poco mejor que si tuvieran un núcleo cada uno (tal vez un poco mejor, tal vez un poco peor). Por lo tanto, los beneficios de la operación paralela desaparecen a medida que el uso cambia de una prueba de solicitud única a una multitud de solicitudes en el mundo real.

11

La respuesta corta es no, no debería usar simplemente Parallel.ForEach o construcciones relacionadas en cada ciclo que pueda. Paralelo tiene algunos gastos generales, lo que no se justifica en loops con pocas e iteraciones rápidas. Además, break es significativamente más complejo dentro de estos bucles.

Parallel.ForEach es una solicitud para programar el ciclo como el planificador de tareas lo considere adecuado, en función del número de iteraciones en el ciclo, el número de núcleos de CPU en el hardware y la carga actual en ese hardware. La ejecución paralela real no siempre está garantizada, y es menos probable si hay menos núcleos, el número de iteraciones es bajo y/o la carga actual es alta.

Ver también Does Parallel.ForEach limits the number of active threads? y Does Parallel.For use one Task per iteration?

La respuesta larga:

Podemos clasificar los bucles de cómo caen en dos ejes:

  1. pocas iteraciones a través de muchas iteraciones.
  2. Cada iteración es rápida hasta que cada iteración es lenta.

Un tercer factor es si las tareas varían mucho en duración: por ejemplo, si está calculando puntos en el conjunto de Mandelbrot, algunos puntos se calculan rápidamente, algunos tardan mucho más.

Cuando hay pocas iteraciones rápidas, probablemente no valga la pena utilizar la paralelización de ninguna manera, lo más probable es que termine más lento debido a los gastos generales. Incluso si la paralelización acelera un bucle pequeño y rápido en particular, es poco probable que sea de interés: las ganancias serán pequeñas y no es un cuello de botella de rendimiento en su aplicación, así que optimice la legibilidad, no el rendimiento.

Cuando un bucle tiene muy pocas iteraciones, lento y desea un mayor control, es posible considerar el uso de Tareas para manejarlos, a lo largo de las líneas de:

var tasks = new List<Task>(actions.Length); 
foreach(var action in actions) 
{ 
    tasks.Add(Task.Factory.StartNew(action)); 
} 
Task.WaitAll(tasks.ToArray()); 

Donde hay muchas iteraciones, Parallel.ForEach se encuentra en su elemento.

Los Microsoft documentation estados que

Cuando se ejecuta un bucle en paralelo, el TPL particiones la fuente de datos de modo que el bucle puede operar en múltiples partes al mismo tiempo. Detrás de las escenas , el Programador de tareas divide la tarea según los recursos del sistema y la carga de trabajo. Cuando sea posible, el planificador redistribuye el trabajo entre varios subprocesos y procesadores si la carga de trabajo se vuelve desequilibrada .

Esta partición y dinámica re-programación va a ser más difícil de hacer efectivamente como el número de iteraciones del bucle disminuye, y es más necesario si las iteraciones varían en duración y en presencia de otras tareas que se ejecutan en el mismo máquina.

Ejecuto algunos códigos.

Los resultados de las pruebas que se muestran a continuación muestran una máquina sin nada más que se ejecute en ella, y no se utilizan otros subprocesos del .Net Thread Pool. Esto no es típico (de hecho, en un escenario de servidor web es muy poco realista). En la práctica, es posible que no vea ninguna paralelización con un pequeño número de iteraciones.

El código de prueba es:

namespace ParallelTests 
{ 
    class Program 
    { 
     private static int Fibonacci(int x) 
     { 
      if (x <= 1) 
      { 
       return 1; 
      } 
      return Fibonacci(x - 1) + Fibonacci(x - 2); 
     } 

     private static void DummyWork() 
     { 
      var result = Fibonacci(10); 
      // inspect the result so it is no optimised away. 
      // We know that the exception is never thrown. The compiler does not. 
      if (result > 300) 
      { 
       throw new Exception("failed to to it"); 
      } 
     } 

     private const int TotalWorkItems = 2000000; 

     private static void SerialWork(int outerWorkItems) 
     { 
      int innerLoopLimit = TotalWorkItems/outerWorkItems; 
      for (int index1 = 0; index1 < outerWorkItems; index1++) 
      { 
       InnerLoop(innerLoopLimit); 
      } 
     } 

     private static void InnerLoop(int innerLoopLimit) 
     { 
      for (int index2 = 0; index2 < innerLoopLimit; index2++) 
      { 
       DummyWork(); 
      } 
     } 

     private static void ParallelWork(int outerWorkItems) 
     { 
      int innerLoopLimit = TotalWorkItems/outerWorkItems; 
      var outerRange = Enumerable.Range(0, outerWorkItems); 
      Parallel.ForEach(outerRange, index1 => 
      { 
       InnerLoop(innerLoopLimit); 
      }); 
     } 

     private static void TimeOperation(string desc, Action operation) 
     { 
      Stopwatch timer = new Stopwatch(); 
      timer.Start(); 
      operation(); 
      timer.Stop(); 

      string message = string.Format("{0} took {1:mm}:{1:ss}.{1:ff}", desc, timer.Elapsed); 
      Console.WriteLine(message); 
     } 

     static void Main(string[] args) 
     { 
      TimeOperation("serial work: 1",() => Program.SerialWork(1)); 
      TimeOperation("serial work: 2",() => Program.SerialWork(2)); 
      TimeOperation("serial work: 3",() => Program.SerialWork(3)); 
      TimeOperation("serial work: 4",() => Program.SerialWork(4)); 
      TimeOperation("serial work: 8",() => Program.SerialWork(8)); 
      TimeOperation("serial work: 16",() => Program.SerialWork(16)); 
      TimeOperation("serial work: 32",() => Program.SerialWork(32)); 
      TimeOperation("serial work: 1k",() => Program.SerialWork(1000)); 
      TimeOperation("serial work: 10k",() => Program.SerialWork(10000)); 
      TimeOperation("serial work: 100k",() => Program.SerialWork(100000)); 

      TimeOperation("parallel work: 1",() => Program.ParallelWork(1)); 
      TimeOperation("parallel work: 2",() => Program.ParallelWork(2)); 
      TimeOperation("parallel work: 3",() => Program.ParallelWork(3)); 
      TimeOperation("parallel work: 4",() => Program.ParallelWork(4)); 
      TimeOperation("parallel work: 8",() => Program.ParallelWork(8)); 
      TimeOperation("parallel work: 16",() => Program.ParallelWork(16)); 
      TimeOperation("parallel work: 32",() => Program.ParallelWork(32)); 
      TimeOperation("parallel work: 64",() => Program.ParallelWork(64)); 
      TimeOperation("parallel work: 1k",() => Program.ParallelWork(1000)); 
      TimeOperation("parallel work: 10k",() => Program.ParallelWork(10000)); 
      TimeOperation("parallel work: 100k",() => Program.ParallelWork(100000)); 

      Console.WriteLine("done"); 
      Console.ReadLine(); 
     } 
    } 
} 

los resultados en una máquina de 4-core Windows 7 son:

serial work: 1 took 00:02.31 
serial work: 2 took 00:02.27 
serial work: 3 took 00:02.28 
serial work: 4 took 00:02.28 
serial work: 8 took 00:02.28 
serial work: 16 took 00:02.27 
serial work: 32 took 00:02.27 
serial work: 1k took 00:02.27 
serial work: 10k took 00:02.28 
serial work: 100k took 00:02.28 

parallel work: 1 took 00:02.33 
parallel work: 2 took 00:01.14 
parallel work: 3 took 00:00.96 
parallel work: 4 took 00:00.78 
parallel work: 8 took 00:00.84 
parallel work: 16 took 00:00.86 
parallel work: 32 took 00:00.82 
parallel work: 64 took 00:00.80 
parallel work: 1k took 00:00.77 
parallel work: 10k took 00:00.78 
parallel work: 100k took 00:00.77 
done 

Ejecución de código compilado en .Net 4 y .Net 4.5 dan la misma resultados.

Las ejecuciones de trabajo en serie son todas iguales. No importa cómo lo cortes, se ejecuta en aproximadamente 2,28 segundos.

El trabajo paralelo con 1 iteración es ligeramente más largo que ningún paralelismo en absoluto. 2 elementos son más cortos, por lo que es 3 y con 4 o más iteraciones es todo alrededor de 0.8 segundos.

Está utilizando todos los núcleos, pero no con el 100% de eficiencia. Si el trabajo en serie se dividió en 4 formas sin sobrecarga, se completaría en 0,57 segundos (2,28/4 = 0,57).

En otros escenarios no vi ninguna aceleración en absoluto con 2 o 3 iteraciones paralelas. No tiene un control detallado sobre eso con Parallel.ForEach y el algoritmo puede decidir "particionarlos" en solo 1 fragmento y ejecutarlo en 1 núcleo si la máquina está ocupada.

0

Estos son mis puntos de referencia que muestran que el serial puro es el más lento, junto con varios niveles de particionamiento.

class Program 
{ 
    static void Main(string[] args) 
    { 
     NativeDllCalls(true, 1, 400000000, 0); // Seconds:  0.67 |) 595,203,995.01 ops 
     NativeDllCalls(true, 1, 400000000, 3); // Seconds:  0.91 |) 439,052,826.95 ops 
     NativeDllCalls(true, 1, 400000000, 4); // Seconds:  0.80 |) 501,224,491.43 ops 
     NativeDllCalls(true, 1, 400000000, 8); // Seconds:  0.63 |) 635,893,653.15 ops 
     NativeDllCalls(true, 4, 100000000, 0); // Seconds:  0.35 |) 1,149,359,562.48 ops 
     NativeDllCalls(true, 400, 1000000, 0); // Seconds:  0.24 |) 1,673,544,236.17 ops 
     NativeDllCalls(true, 10000, 40000, 0); // Seconds:  0.22 |) 1,826,379,772.84 ops 
     NativeDllCalls(true, 40000, 10000, 0); // Seconds:  0.21 |) 1,869,052,325.05 ops 
     NativeDllCalls(true, 1000000, 400, 0); // Seconds:  0.24 |) 1,652,797,628.57 ops 
     NativeDllCalls(true, 100000000, 4, 0); // Seconds:  0.31 |) 1,294,424,654.13 ops 
     NativeDllCalls(true, 400000000, 0, 0); // Seconds:  1.10 |) 364,277,890.12 ops 
    } 


static void NativeDllCalls(bool useStatic, int nonParallelIterations, int parallelIterations = 0, int maxParallelism = 0) 
{ 
    if (useStatic) { 
     Iterate<string, object>(
      (msg, cntxt) => { 
       ServiceContracts.ForNativeCall.SomeStaticCall(msg); 
      } 
      , "test", null, nonParallelIterations,parallelIterations, maxParallelism); 
    } 
    else { 
     var instance = new ServiceContracts.ForNativeCall(); 
     Iterate(
      (msg, cntxt) => { 
       cntxt.SomeCall(msg); 
      } 
      , "test", instance, nonParallelIterations, parallelIterations, maxParallelism); 
    } 
} 

static void Iterate<T, C>(Action<T, C> action, T testMessage, C context, int nonParallelIterations, int parallelIterations=0, int maxParallelism= 0) 
{ 
    var start = DateTime.UtcNow;    
    if(nonParallelIterations == 0) 
     nonParallelIterations = 1; // normalize values 

    if(parallelIterations == 0) 
     parallelIterations = 1; 

    if (parallelIterations > 1) {      
     ParallelOptions options; 
     if (maxParallelism == 0) // default max parallelism 
      options = new ParallelOptions(); 
     else 
      options = new ParallelOptions { MaxDegreeOfParallelism = maxParallelism }; 

     if (nonParallelIterations > 1) { 
      Parallel.For(0, parallelIterations, options 
      , (j) => { 
       for (int i = 0; i < nonParallelIterations; ++i) { 
        action(testMessage, context); 
       } 
      }); 
     } 
     else { // no nonParallel iterations 
      Parallel.For(0, parallelIterations, options 
      , (j) => {       
       action(testMessage, context); 
      }); 
     } 
    } 
    else { 
     for (int i = 0; i < nonParallelIterations; ++i) { 
      action(testMessage, context); 
     } 
    } 

    var end = DateTime.UtcNow; 

    Console.WriteLine("\tSeconds: {0,8:0.00} |) {1,16:0,000.00} ops", 
     (end - start).TotalSeconds, (Math.Max(parallelIterations, 1) * nonParallelIterations/(end - start).TotalSeconds)); 

} 

} 
Cuestiones relacionadas