delphi parallel-processing rtl-ppl

delphi - Extraño comportamiento de TParallel.Por defecto ThreadPool



parallel-processing rtl-ppl (1)

Estoy probando las funciones de programación paralela de Delphi XE7 Update 1.

TParallel.For loop TParallel.For simple que básicamente hace algunas operaciones falsas para pasar el tiempo.

Lancé el programa en una vCPU de 36 en una instancia de AWS (c4.8xlarge) para tratar de ver cuál podría ser la ganancia de la Programación en Paralelo.

Cuando TParallel.For el programa por primera vez y ejecuto el ciclo TParallel.For , veo una ganancia significativa (aunque admitelly mucho menos de lo que anticipé con 36 vCPU):

Parallel matches: 23077072 in 242ms Single Threaded matches: 23077072 in 2314ms

Si no cierro el programa y ejecuto el pase nuevamente en la máquina 36 vCPU poco después (por ejemplo, inmediatamente o unos 10-20 segundos más tarde), el pase paralelo empeora mucho:

Parallel matches: 23077169 in 2322ms Single Threaded matches: 23077169 in 2316ms

Si no cierro el programa y espero unos minutos (no unos segundos, sino unos minutos) antes de volver a ejecutar el pase, vuelvo a obtener los resultados que obtengo al iniciar el programa por primera vez (una mejora de 10 veces en el tiempo de respuesta) .

El primer pase justo después de iniciar el programa siempre es más rápido en la máquina de 36 vCPU, por lo que parece que este efecto solo ocurre la segunda vez que se llama a TParallel.For en el programa.

Este es el código de muestra que estoy ejecutando:

unit ParallelTests; interface uses Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics, System.Threading, System.SyncObjs, System.Diagnostics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls; type TForm1 = class(TForm) Button1: TButton; Memo1: TMemo; SingleThreadCheckBox: TCheckBox; ParallelCheckBox: TCheckBox; UnitsEdit: TEdit; Label1: TLabel; procedure Button1Click(Sender: TObject); private { Private declarations } public { Public declarations } end; var Form1: TForm1; implementation {$R *.dfm} procedure TForm1.Button1Click(Sender: TObject); var matches: integer; i,j: integer; sw: TStopWatch; maxItems: integer; referenceStr: string; begin sw := TStopWatch.Create; maxItems := 5000; Randomize; SetLength(referenceStr,120000); for i := 1 to 120000 do referenceStr[i] := Chr(Ord(''a'') + Random(26)); if ParallelCheckBox.Checked then begin matches := 0; sw.Reset; sw.Start; TParallel.For(1, MaxItems, procedure (Value: Integer) var index: integer; found: integer; begin found := 0; for index := 1 to length(referenceStr) do begin if (((Value mod 26) + ord(''a'')) = ord(referenceStr[index])) then begin inc(found); end; end; TInterlocked.Add(matches, found); end); sw.Stop; Memo1.Lines.Add(''Parallel matches: '' + IntToStr(matches) + '' in '' + IntToStr(sw.ElapsedMilliseconds) + ''ms''); end; if SingleThreadCheckBox.Checked then begin matches := 0; sw.Reset; sw.Start; for i := 1 to MaxItems do begin for j := 1 to length(referenceStr) do begin if (((i mod 26) + ord(''a'')) = ord(referenceStr[j])) then begin inc(matches); end; end; end; sw.Stop; Memo1.Lines.Add(''Single Threaded matches: '' + IntToStr(Matches) + '' in '' + IntToStr(sw.ElapsedMilliseconds) + ''ms''); end; end; end.

¿Esto está funcionando como está diseñado? Encontré este artículo ( http://365ball.co.uk/tag/parallel-programming/ ) recomendando que dejara que la biblioteca decidiera el grupo de subprocesos, pero no veo el sentido de usar programación paralela si tengo que esperar unos minutos de solicitud a solicitud para que la solicitud se sirva más rápido.

¿Me falta algo sobre cómo se supone que debe usarse un bucle TParallel.For ?

Tenga en cuenta que no puedo reproducir esto en una instancia de AWS m3.large (2 vCPU según AWS). En ese caso, siempre obtengo una ligera mejora, y no obtengo un resultado peor en las llamadas posteriores de TParallel.For poco tiempo después.

Parallel matches: 23077054 in 2057ms Single Threaded matches: 23077054 in 2900ms

Por lo tanto, parece que este efecto ocurre cuando hay muchos núcleos disponibles (36), lo cual es una pena porque el objetivo de la Programación Paralela es beneficiarse de muchos núcleos. Me pregunto si esto es un error de la biblioteca debido a la alta cantidad de núcleos o al hecho de que el recuento de núcleos no es una potencia de 2 en este caso.

ACTUALIZACIÓN: después de probarlo con varias instancias de recuentos de vCPU diferentes en AWS, este parece ser el comportamiento:

  • 36 vCPU (c4.8xlarge) . Tienes que esperar minutos entre llamadas subsiguientes a una llamada vana TAL (eso la hace inutilizable para producción)
  • 32 vCPU (c3.8xlarge) . Tienes que esperar minutos entre llamadas subsiguientes a una llamada vana TAL (eso la hace inutilizable para producción)
  • 16 vCPUs (c3.4xlarge) . Tienes que esperar sub segundas veces. Podría ser útil si la carga es baja pero el tiempo de respuesta sigue siendo importante
  • 8 vCPUs (c3.2xlarge) . Parece que funciona normalmente
  • 4 vCPUs (c3.xlarge) . Parece que funciona normalmente
  • 2 vCPUs (m3.large) . Parece que funciona normalmente

Creé dos programas de prueba, basados ​​en el tuyo, para comparar System.Threading y OTL . Construí con XE7 actualización 1 y OTL r1397. La fuente OTL que utilicé corresponde a la versión 3.04. Construí con el compilador de Windows de 32 bits, usando las opciones de compilación de lanzamiento.

Mi máquina de prueba es una Intel Xeon E5530 doble con Windows 7 x64. El sistema tiene dos procesadores de cuatro núcleos. Eso es 8 procesadores en total, pero el sistema dice que hay 16 debido a hiper-threading. La experiencia me dice que el hyper-threading no es más que mercadillo y nunca he visto escalar más allá de un factor de 8 en esta máquina.

Ahora para los dos programas, que son casi idénticos.

System.Threading

program SystemThreadingTest; {$APPTYPE CONSOLE} uses System.Diagnostics, System.Threading; const maxItems = 5000; DataSize = 100000; procedure DoTest; var matches: integer; i, j: integer; sw: TStopWatch; referenceStr: string; begin Randomize; SetLength(referenceStr, DataSize); for i := low(referenceStr) to high(referenceStr) do referenceStr[i] := Chr(Ord(''a'') + Random(26)); // parallel matches := 0; sw := TStopWatch.StartNew; TParallel.For(1, maxItems, procedure(Value: integer) var index: integer; found: integer; begin found := 0; for index := low(referenceStr) to high(referenceStr) do if (((Value mod 26) + Ord(''a'')) = Ord(referenceStr[index])) then inc(found); AtomicIncrement(matches, found); end); Writeln(''Parallel matches: '', matches, '' in '', sw.ElapsedMilliseconds, ''ms''); // serial matches := 0; sw := TStopWatch.StartNew; for i := 1 to maxItems do for j := low(referenceStr) to high(referenceStr) do if (((i mod 26) + Ord(''a'')) = Ord(referenceStr[j])) then inc(matches); Writeln(''Serial matches: '', matches, '' in '', sw.ElapsedMilliseconds, ''ms''); end; begin while True do DoTest; end.

OTL

program OTLTest; {$APPTYPE CONSOLE} uses Winapi.Windows, Winapi.Messages, System.Diagnostics, OtlParallel; const maxItems = 5000; DataSize = 100000; procedure ProcessThreadMessages; var msg: TMsg; begin while PeekMessage(Msg, 0, 0, 0, PM_REMOVE) and (Msg.Message <> WM_QUIT) do begin TranslateMessage(Msg); DispatchMessage(Msg); end; end; procedure DoTest; var matches: integer; i, j: integer; sw: TStopWatch; referenceStr: string; begin Randomize; SetLength(referenceStr, DataSize); for i := low(referenceStr) to high(referenceStr) do referenceStr[i] := Chr(Ord(''a'') + Random(26)); // parallel matches := 0; sw := TStopWatch.StartNew; Parallel.For(1, maxItems).Execute( procedure(Value: integer) var index: integer; found: integer; begin found := 0; for index := low(referenceStr) to high(referenceStr) do if (((Value mod 26) + Ord(''a'')) = Ord(referenceStr[index])) then inc(found); AtomicIncrement(matches, found); end); Writeln(''Parallel matches: '', matches, '' in '', sw.ElapsedMilliseconds, ''ms''); ProcessThreadMessages; // serial matches := 0; sw := TStopWatch.StartNew; for i := 1 to maxItems do for j := low(referenceStr) to high(referenceStr) do if (((i mod 26) + Ord(''a'')) = Ord(referenceStr[j])) then inc(matches); Writeln(''Serial matches: '', matches, '' in '', sw.ElapsedMilliseconds, ''ms''); end; begin while True do DoTest; end.

Y ahora la salida.

Salida de System.Threading

Parallel matches: 19230817 in 374ms Serial matches: 19230817 in 2423ms Parallel matches: 19230698 in 374ms Serial matches: 19230698 in 2409ms Parallel matches: 19230556 in 368ms Serial matches: 19230556 in 2433ms Parallel matches: 19230635 in 2412ms Serial matches: 19230635 in 2430ms Parallel matches: 19230843 in 2441ms Serial matches: 19230843 in 2413ms Parallel matches: 19230905 in 2493ms Serial matches: 19230905 in 2423ms Parallel matches: 19231032 in 2430ms Serial matches: 19231032 in 2443ms Parallel matches: 19230669 in 2440ms Serial matches: 19230669 in 2473ms Parallel matches: 19230811 in 2404ms Serial matches: 19230811 in 2432ms ....

Salida OTL

Parallel matches: 19230667 in 422ms Serial matches: 19230667 in 2475ms Parallel matches: 19230663 in 335ms Serial matches: 19230663 in 2438ms Parallel matches: 19230889 in 395ms Serial matches: 19230889 in 2461ms Parallel matches: 19230874 in 391ms Serial matches: 19230874 in 2441ms Parallel matches: 19230617 in 385ms Serial matches: 19230617 in 2524ms Parallel matches: 19231021 in 368ms Serial matches: 19231021 in 2455ms Parallel matches: 19230904 in 357ms Serial matches: 19230904 in 2537ms Parallel matches: 19230568 in 373ms Serial matches: 19230568 in 2456ms Parallel matches: 19230758 in 333ms Serial matches: 19230758 in 2710ms Parallel matches: 19230580 in 371ms Serial matches: 19230580 in 2532ms Parallel matches: 19230534 in 336ms Serial matches: 19230534 in 2436ms Parallel matches: 19230879 in 368ms Serial matches: 19230879 in 2419ms Parallel matches: 19230651 in 409ms Serial matches: 19230651 in 2598ms Parallel matches: 19230461 in 357ms ....

Dejé funcionando la versión OTL durante mucho tiempo y el patrón nunca cambió. La versión paralela siempre fue alrededor de 7 veces más rápida que la serial.

Conclusión

El código es asombrosamente simple. La única conclusión razonable que se puede extraer es que la implementación de System.Threading es defectuosa.

Ha habido numerosos informes de errores relacionados con la nueva biblioteca System.Threading . Todos los signos son que su calidad es pobre. Embarcadero tiene un largo historial de lanzamiento de código de biblioteca por debajo del estándar. Estoy pensando en TMonitor , el asistente de cadenas XE3, versiones anteriores de System.IOUtils , FireMonkey. La lista continua.

Parece claro que la calidad es un gran problema con Embarcadero. Se lanza un código que claramente no ha sido probado adecuadamente, en todo caso. Esto es especialmente problemático para una biblioteca de subprocesos donde los errores pueden permanecer inactivos y solo se exponen en configuraciones específicas de hardware / software. La experiencia de TMonitor me lleva a pensar que Embarcadero no tiene experiencia suficiente para producir código de enhebrado correcto y de alta calidad.

Mi consejo es que no deberías usar System.Threading en su forma actual. Hasta el momento en que pueda verse que tiene la calidad y corrección suficientes, debe evitarse. Sugiero que uses OTL.

EDITAR: La versión original de OTL del programa tuvo una fuga de memoria activa que se produjo debido a un feo detalle de implementación. Paralelo.Para crea tareas con el modificador .Unobserved. Esto hace que dichas tareas solo se destruyan cuando alguna ventana de mensaje interno recibe un mensaje de ''tarea ha terminado''. Esta ventana se crea en el mismo hilo que el Paralelo.Para quien llama, es decir, en el hilo principal en este caso. Como el hilo principal no era el procesamiento de mensajes, las tareas nunca se destruyeron y el consumo de memoria (más otros recursos) simplemente se acumuló. Es posible que debido a ese programa se cuelgue después de un tiempo.