【问题标题】:Strange behaviour of TParallel.For default ThreadPoolTParallel.For 默认线程池的奇怪行为
【发布时间】:2015-03-15 15:53:41
【问题描述】:

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

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

我在 AWS 实例 (c4.8xlarge) 上的 36 个 vCPU 上启动了该程序,以尝试了解并行编程的好处。

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

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

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

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

如果我不关闭程序并等待几分钟(不是几秒钟,而是几分钟),然后再次运行传递,我会再次获得第一次启动程序时得到的结果(10 倍改进响应时间)。

在 36 个 vCPU 的机器上,启动程序后的第一遍总是更快,所以这种效果似乎只在程序中第二次调用 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)。您必须在后续调用普通 TParallel 调用之间等待几分钟(这使得它无法用于 生产)
  • 32 个 vCPU (c3.8xlarge)。您必须在后续调用普通 TParallel 调用之间等待几分钟(这使得它无法用于 生产)
  • 16 个 vCPU (c3.4xlarge)。您必须等待第二次。如果负载低但响应时间仍然很重要,它可能是可用的
  • 8 个 vCPU (c3.2xlarge)。它似乎可以正常工作
  • 4 个 vCPU (c3.xlarge)。它似乎可以正常工作
  • 2 个 vCPU (m3.large)。它似乎可以正常工作

【问题讨论】:

  • @Pep 如果您认为库有问题,请使用另一个库编写代码并进行比较。我怀疑图书馆是问题所在。
  • 我对它进行了更多测试。看来,至少对于 AWS,Parallel 库在 vCPU > 8 时会出现一些问题。vCPU = 16 时,它比 vCPU = 32 或 36 时要好得多,但它仍然存在问题。可能 TParallel.For 调用已针对最多 8 个虚拟内核(台式机)的系统进行了微调。我会用我的发现更新问题。
  • 这不太可能。我不知道你为什么会这么想。不要猜测。在 OTL 下运行等效代码,看看会发生什么。
  • 所以,我认为该库不会针对特定数量的内核进行优化。但我认为图书馆可能是问题的根源。如果是这样的话,与 OTL 的比较会给出一个好主意。我必须说的是,新的 RTL 并行库完全是垃圾。这里有无数的帖子表明它实施得非常糟糕。我怀疑我能否让自己使用它。我向你推荐 OTL。
  • 我认为很明显,在某些时候您遇到了并行库中的错误,导致代码串行执行。这肯定不是设计使然。肯定不是因为调整错误。这肯定要归结为粗制滥造的实施。坦率地说,Embarcadero 在生成正确的线程代码方面有着糟糕的记录。在TMonitor 的惨败之后,还有人相信他们吗?

标签: delphi parallel-processing rtl-ppl


【解决方案1】:

我根据您的创建了两个测试程序,用于比较 System.ThreadingOTL。我使用 XE7 更新 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 输出

并行匹配:19230817 in 374ms 串行匹配:19230817 in 2423ms 并行匹配:19230698 in 374ms 串行匹配:19230698 in 2409ms 并行匹配:19230556 in 368ms 串行匹配:19230556 in 2433ms 并行匹配:19230635 in 2412ms 串行匹配:19230635 in 2430ms 并行匹配:19230843 in 2441ms 串行匹配:19230843 in 2413ms 并行匹配:19230905 in 2493ms 串行匹配:19230905 in 2423ms 并行匹配:19231032 in 2430ms 串行匹配:19231032 in 2443ms 并行匹配:19230669 in 2440ms 串行匹配:19230669 in 2473ms 并行匹配:19230811 in 2404ms 串行匹配:19230811 in 2432ms ……

OTL 输出

并行匹配:19230667 in 422ms 串行匹配:19230667 in 2475ms 并行匹配:335ms 内 19230663 串行匹配:19230663 in 2438ms 并行匹配:19230889 in 395ms 串行匹配:19230889 in 2461ms 并行匹配:391ms 内 19230874 串行匹配:19230874 in 2441ms 并行匹配:19230617 in 385ms 串行匹配:19230617 in 2524ms 并行匹配:19231021 in 368ms 串行匹配:19231021 in 2455ms 并行匹配:19230904 in 357ms 串行匹配:19230904 in 2537ms 并行匹配:19230568 in 373ms 串行匹配:19230568 in 2456ms 并行匹配:19230758 in 333ms 串行匹配:19230758 in 2710ms 并行匹配:371ms 内 19230580 串行匹配:19230580 in 2532ms 并行匹配:336ms 内 19230534 串行匹配:19230534 in 2436ms 并行匹配:19230879 in 368ms 串行匹配:19230879 in 2419ms 并行匹配:409ms 内 19230651 串行匹配:19230651 in 2598ms 并行匹配:19230461 in 357ms ……

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

结论

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

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

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

我的建议是你不应该使用当前形式的System.Threading。在可以看出它具有足够的质量和正确性之前,应该回避它。我建议你使用 OTL。


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

【讨论】:

  • @David 关于 EMB 经验(或缺乏经验)的 cmets 似乎有点夸大其词。 OTL 有大量的错误报告、竞争条件等。我不会将其解释为 Primoz“没有足够的专业知识来生成高质量、正确的线程代码”。这只是意味着线程库很难,而且通常在有人报告错误之前它是完全出乎意料的。只要 EMB 努力改进图书馆,这是我们最现实的希望。
  • @DaveNovo 如果它只是线程库。但是,Emba 最近发布的库代码中普遍存在低质量问题。 TMonitor 尤其糟糕。 XE3 字符串助手令人震惊。如果代码经过测试甚至执行,就会发现许多错误。留有空白实现的方法。我坚持我所说的。质量很差。线程库现在不适合生产使用。我预计早期版本的 OTL 也存在质量问题。
  • 批评不是针对个人的。我对@Allen 表示最崇高的敬意。我的批评是针对有目共睹的质量问题。我真的希望提高产品质量。我想为此做出贡献。报告严重的错误(如 SetMXCSR/Set8087CW 非线程安全,对 FloatToText 的后续影响)并看到错误仍未修复是令人沮丧的。出了什么问题?
  • @Allen Bauer 基于 CPU 使用率的动态线程池在涉及 I/O 或 GPU 时非常脆弱。十年前的教训。您可能会在等待 I/O(低 CPU 使用率)时完成堆积工作,而当 I/O 数据开始流动时,您的线程过多,CPU 缓存不足等。冲洗并重复。
  • @Allen Bauer 可能不是微不足道的,I/O 任务可以是 I/O-then-process 类型(从 DB 加载然后处理然后保存到 DB,从文件加载然后处理然后I/O 到 GPU 等),因此它还需要分解任务。另一个重要的问题是在 VM 中运行时获得有意义的 CPU 使用率测量。
猜你喜欢
  • 2018-09-30
  • 2014-03-25
  • 2012-12-23
  • 1970-01-01
  • 2018-01-15
  • 2018-03-06
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多