He encontrado un comportamiento extraño en una aplicación .NET que realiza un procesamiento altamente paralelo en un conjunto de datos en memoria.Escalado no lineal de operaciones .NET en máquinas multi-core
Cuando se ejecuta en un procesador multi-core (IntelCore2 Quad Q6600 2.4GHz) exhibe una escala no lineal ya que varios hilos se inician para procesar los datos.
Cuando se ejecuta como un bucle no multiproceso en un solo núcleo, el proceso puede completar aproximadamente 2,4 millones de cálculos por segundo. Cuando se ejecuta como cuatro hilos esperaría cuatro veces más rendimiento, en algún lugar en la vecindad de 9 millones de cálculos por segundo, pero ¡ay !, no. En la práctica, solo completa unos 4.1 millones por segundo ... bastante corto del rendimiento esperado.
Además, el comportamiento se produce independientemente de que use PLINQ, un grupo de subprocesos o cuatro subprocesos creados explícitamente. Muy extraño ...
Nada más se está ejecutando en la máquina usando el tiempo de CPU, ni hay bloqueos u otros objetos de sincronización involucrados en el cálculo ... simplemente debería avanzar a través de los datos. Lo he confirmado (en la medida de lo posible) mirando los datos perfmon mientras se ejecuta el proceso ... y no hay conflictos de hilos informados ni actividad de recolección de basura.
mis teorías en la actualidad
- la sobrecarga de todas las técnicas (contexto hilo interruptores, etc) se abrumadora los cálculos
- Los hilos no se están asignados a cada uno de los cuatro núcleos y pase algún tiempo esperando en el mismo núcleo del procesador ... no estoy seguro de cómo probar esta teoría ...
- .NET Los subprocesos CLR no se ejecutan con la prioridad esperada o tienen algún gasto interno oculto.
A continuación se muestra un extracto representativo del código que debe exhibir el mismo comportamiento:
var evaluator = new LookupBasedEvaluator();
// find all ten-vertex polygons that are a subset of the set of points
var ssg = new SubsetGenerator<PolygonData>(Points.All, 10);
const int TEST_SIZE = 10000000; // evaluate the first 10 million records
// materialize the data into memory...
var polygons = ssg.AsParallel()
.Take(TEST_SIZE)
.Cast<PolygonData>()
.ToArray();
var sw1 = Stopwatch.StartNew();
// for loop completes in about 4.02 seconds... ~ 2.483 million/sec
foreach(var polygon in polygons)
evaluator.Evaluate(polygon);
s1.Stop();
Console.WriteLine("Linear, single core loop: {0}", s1.ElapsedMilliseconds);
// now attempt the same thing in parallel using Parallel.ForEach...
// MS documentation indicates this internally uses a worker thread pool
// completes in 2.61 seconds ... or ~ 3.831 million/sec
var sw2 = Stopwatch.StartNew();
Parallel.ForEach(polygons, p => evaluator.Evaluate(p));
sw2.Stop();
Console.WriteLine("Parallel.ForEach() loop: {0}", s2.ElapsedMilliseconds);
// now using PLINQ, er get slightly better results, but not by much
// completes in 2.21 seconds ... or ~ 4.524 million/second
var sw3 = Stopwatch.StartNew();
polygons.AsParallel(Environment.ProcessorCount)
.AsUnordered() // no sure this is necessary...
.ForAll(h => evalautor.Evaluate(h));
sw3.Stop();
Console.WriteLine("PLINQ.AsParallel.ForAll: {0}", s3.EllapsedMilliseconds);
// now using four explicit threads:
// best, still short of expectations at 1.99 seconds = ~ 5 million/sec
ParameterizedThreadStart tsd = delegate(object pset) { foreach (var p in (IEnumerable<Card[]>) pset) evaluator.Evaluate(p); };
var t1 = new Thread(tsd);
var t2 = new Thread(tsd);
var t3 = new Thread(tsd);
var t4 = new Thread(tsd);
var sw4 = Stopwatch.StartNew();
t1.Start(hands);
t2.Start(hands);
t3.Start(hands);
t4.Start(hands);
t1.Join();
t2.Join();
t3.Join();
t4.Join();
sw.Stop();
Console.WriteLine("Four Explicit Threads: {0}", s4.EllapsedMilliseconds);
Esto podría haberse resuelto usando un grupo de recursos. Esta pregunta me ayudó a entender por qué las agrupaciones pueden ser importantes cuando se intenta realizar operaciones paralelas masivas. – Will
Sp Su implementación original no paralela se ejecuta a 2.4Mop/s, su última versión paralela en 4 núcleos se ejecuta a 12.2Mop/s. Eso es escalar súper lineal, que es notable y digno de investigación. Vuelve a probar la ejecución del código de núcleo único una vez que realizó el cambio, ¿verdad? –
Al cambiar la asignación de memoria se mejoró el rendimiento de un solo núcleo a 3,2 Mops, por lo que los resultados de 12,2 núcleos son razonables. – LBushkin