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:
- pocas iteraciones a través de muchas iteraciones.
- 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.
¿Qué contiene la matriz? El enfoque depende de lo que intenta hacer con los artículos. – jgauffin