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.