题 TParallel的奇怪行为。对于默认的ThreadPool


我正在尝试Delphi XE7 Update 1的并行编程功能。

我创造了一个简单的 TParallel.For 循环,基本上做一些虚假的操作来打发时间。

我在AWS实例(c4.8xlarge)上的36 vCPU上启动了该程序,试图了解并行编程的收益。

当我第一次启动程序并执行时 TParallel.For 循环,我看到了显着的收益(虽然admitelly比我预期的36个vCPU少很多):

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

如果我不关闭程序并在不久之后再次在36 vCPU机器上运行传递(例如,立即或大约10-20秒后),并行传递会恶化很多:

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

如果我没有关闭程序并等待几分钟(不是几秒钟,但几分钟)再次运行传递之前,我再次得到第一次启动程序时得到的结果(响应时间提高了10倍) 。

启动程序后的第一次通过在36个vCPU机器上总是更快,所以看起来这个效果只发生在第二次a TParallel.For 在程序中调用。

这是我正在运行的示例代码:

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.

这是按设计工作的吗?我找到了这篇文章(http://delphiaball.co.uk/tag/parallel-programming/)建议我让库决定线程池,但如果我必须等待几分钟从请求到请求,我就不会看到使用并行编程的重点,以便更快地提供请求。

我错过了任何关于如何做的事情 TParallel.For 应该使用循环?

请注意,我无法在AWS m3.large实例(根据AWS的2个vCPU)上重现此问题。在那种情况下,我总是得到一点改善,而且在随后的调用中我没有得到更糟糕的结果 TParallel.For 不久之后。

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

因此,当有许多可用内核(36)时,似乎会出现这种影响,这很可惜,因为并行编程的整个要点是要从许多内核中受益。我想知道这是一个库错误,因为核心数量很多,或者核心数量在这种情况下不是2的幂。

更新:使用不同vCPU的各种实例对其进行测试   在AWS中计算,这似乎是行为:

  • 36个vCPU(c4.8xlarge)。您必须在后续调用vanilla TParallel调用之间等待几分钟(这使得它无法使用   生产)
  • 32个vCPU(c3.8xlarge)。您必须在后续调用vanilla TParallel调用之间等待几分钟(这使得它无法使用   生产)
  • 16个vCPU(c3.4xlarge)。你必须等二次。如果负载低但响应时间仍然很重要,它可以使用
  • 8个vCPU(c3.2xlarge)。它似乎正常工作
  • 4个vCPU(c3.xlarge)。它似乎正常工作
  • 2个vCPU(m3.large)。它似乎正常工作

17
2018-03-15 15:53


起源


@Pep如果您认为库是个问题,请使用其他库编写代码并进行比较。我怀疑图书馆是个问题。 - David Heffernan
我测试了一下。看来,至少对于AWS,当vCPU> 8时,并行库存在一些问题。对于vCPU = 16,它比vCPU = 32或36更好地工作,但它仍然存在问题。对于最多8个虚拟核心(台式机)的系统,TParallel.For呼叫已被微调。我将用我的发现更新问题。 - Pep
这根本不可能。我不知道为什么你会这么想。不要猜。在OTL下运行等效代码,看看会发生什么。 - David Heffernan
所以,我认为该库不会针对一定数量的内核进行优化。但我认为图书馆可能是问题的根源。如果是这种情况,与OTL的比较会给出一个好主意。我必须说的是,新的RTL并行库完全是垃圾。在这里有无数的帖子暴露它实施得非常糟糕。我怀疑我能不能使用它。我向你推荐OTL。 - David Heffernan
我认为很明显,在某些时候你会遇到并行库中的一个错误导致代码串行执行。这肯定不是设计上的。当然不是由于调整错误。这肯定是伪劣的实施。坦率地说,Embarcadero在制作正确的线程代码方面有着可怕的记录。在崩溃之后 TMonitor,谁能相信他们呢? - David Heffernan


答案:


我创建了两个基于你的测试程序来进行比较 System.Threading 和 OTL。我使用XE7 update 1和OTL r1397构建。我使用的OTL源对应于3.04版。我使用32位Windows编译器构建,使用发布版本选项。

我的测试机器是运行Windows 7 x64的双Intel Xeon E5530。该系统有两个四核处理器。总共有8个处理器,但系统表示由于超线程而有16个处理器。经验告诉我,超线程只是营销方式,而且我从未在这台机器上看到超过8倍的扩展。

现在两个程序几乎完全相同。

的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.

而现在的输出。

System.Threading输出

平行匹配:在374ms内为19230817
串行匹配:19230817,2243ms
平行匹配:在374ms内为19230698
串行匹配:19230698在2409ms
平行匹配:在368ms内为19230556
串行匹配:19233556在2433ms
平行匹配:在2412ms内为19230635
串行匹配:2430ms的19230635
平行匹配:在2441ms内为19230843
串行匹配:19213843在2413ms
平行匹配:在2493ms的19230905
串行匹配:19230905在2423ms
平行比赛:19231032年2430毫秒
系列匹配:19231032在2443ms
平行匹配:2440毫秒的19230669
串行匹配:19273669在2473ms
并行匹配:在2404ms内为19230811
串行匹配:在2432ms内为19230811
....

OTL输出

并行匹配:在422ms内为19230667
串行匹配:19275667,2475ms
并行匹配:在335ms内为19230663
串行匹配:19230663在2438ms
平行匹配:19230889在395ms
串行匹配:19230889在2461ms
平行匹配:19230874在391ms
串行匹配:19241874在2441ms
平行匹配:在385ms内为19230617
串行匹配:19224617在2524ms
平行比赛:19231021在368ms
系列赛:19231021,2455ms
平行匹配:在357ms内的19230904
串行匹配:19237904在2537ms
平行匹配:在373ms内为19230568
串行匹配:19235568在2456ms
并行匹配:19230758在333ms
串行匹配:19230758在2710ms
平行匹配:在371ms内为19230580
串行匹配:19232580在2532ms
并行匹配:在336毫秒的19230534
串行匹配:19236534在2436ms
平行匹配:在368ms内为19230879
串行匹配:19230879在2419ms
并行匹配:在409ms内为19230651
串行匹配:19230651在2598ms
平行匹配:在357ms内为19230461
....

我让OTL版本运行了很长时间,模式从未改变过。并行版本总是比串行版快7倍左右。

结论

代码非常简单。可以得出的唯一合理结论是实施 System.Threading 是有缺陷的。

有许多关于新的错误报告 System.Threading 图书馆。所有的迹象都表明它的质量很差。 Embarcadero在发布不合标准的库代码方面有着悠久的历史记录。我在想 TMonitor,XE3字符串助手,早期版本 System.IOUtils,FireMonkey。名单还在继续。

很明显,Embarcadero的质量是一个大问题。代码发布,很明显没有经过充分测试,如果有的话。这对于线程库来说尤其麻烦,其中错误可以处于休眠状态并且仅在特定的硬件/软件配置中暴露。来自的经验 TMonitor让我相信Embarcadero没有足够的专业知识来生产高质量,正确的线程代码。

我的建议是你不应该使用 System.Threading 目前的形式。直到可以看出它具有足够的质量和正确性的时候,它应该被避开。我建议你使用OTL。


编辑:该程序的原始OTL版本有一个实时内存泄漏,这是由于一个丑陋的实现细节。 Parallel.For使用.Unobserved修饰符创建任务。这导致所述任务仅在某个内部消息窗口收到“任务已终止”消息时被销毁。该窗口在与Parallel.For调用者相同的线程中创建 - 即在这种情况下在主线程中。由于主线程没有处理消息,任务从未被破坏,内存消耗(加上其他资源)只是堆积起来。有可能因为那个程序在一段时间后被绞死了。


15
2018-03-16 09:23



@David关于EMBs体验(或缺乏体验)的评论似乎有点夸张。 OTL已经有大量的错误报告,竞争条件等等。我不认为这意味着Primoz“没有足够的专业知识来生成高质量,正确的线程代码”。它只是意味着线程库很难,并且通常直到有人报告错误,这是完全无法预料的。只要EMB正在努力改进图书馆,这是我们现实中最需要的。 - Dave Novo
@DaveNovo如果它只是线程库。但是,Emba最近的图书馆代码发布质量很差。 TMonitor特别糟糕。 XE3字符串助手令人震惊。已发现的许多错误已经过代码测试,甚至执行。留下空白实现的方法。我支持我说的话。质量很差。线程库现在不适合生产使用。我希望早期版本的OTL也存在质量问题。 - David Heffernan
批评不是个人意义上的。我非常重视@Allen。我的批评是针对所有人都清楚看到的质量问题。我真的希望产品质量得到改善。我想为此做出贡献。然后报告像SetMXCSR / Set8087CW非线程安全这样的关键错误,随之而来的对FloatToText的影响,以及看到错误仍未修复,这是令人沮丧的。出了什么问题? - David Heffernan
@Allen Bauer基于CPU使用的动态线程池一旦涉及I / O或GPU就非常脆弱。十年前的经验教训。您可以在等待I / O(CPU使用率低)时最终堆积工作,并且当I / O数据开始流动时,您有太多的线程,使CPU缓存匮乏等。冲洗并重复。 - Eric Grange
@Allen Bauer可能不是很简单,I / O任务可以是I / O然后进程类型(从DB加载然后进程然后保存到DB,从文件加载然后处理然后I / O到GPU等) )所以它也需要分解任务。另一个非常重要的问题是在VM中运行时获得有意义的CPU使用率测量。 - Eric Grange