并行处理字符串Delphi完全可用的CPU使用率

时间:2015-01-22 00:22:58

标签: multithreading delphi parallel-processing delphi-xe6

目标是在单个Delphi应用程序中将浮点数转换为字符串,以实现可用内核的完全使用。我认为这个问题适用于字符串的一般处理。然而在我的例子中,我特意使用了FloatToStr方法。

我正在做什么(我保持这一点非常简单,因此实施方面几乎没有含糊之处):

  • 使用Delphi XE6
  • 创建从TThread继承的线程对象,并启动它们。
  • 在线程执行过程中,它将转换大量的 通过FloatToStr方法加倍到字符串。
  • 为了简化,这些双打只是相同的常数,所以没有 线程所需的共享或全局内存资源。

虽然使用了多个内核,但CPU使用率%总是会超出单个内核的数量。我知道这是一个既定问题。所以我有一些具体的问题。

以简单的方式,可以通过多个应用实例完成相同的操作,从而实现对可用CPU的更充分利用。是否可以在同一个可执行文件中有效地执行此操作? 即在操作系统级别上分配线程不同的进程ID还是由OS识别的某些等效分区?或者这是开箱即用的Delphi无法实现的吗?

范围: 我知道有不同的内存管理器可用&其他小组尝试更改一些较低级别的asm锁定使用情况http://synopse.info/forum/viewtopic.php?id=57 但是,我在不做这么低级别的事情的范围内问这个问题。

由于


嗨J.我的代码故意很简单:

TTaskThread = class(TThread)
public
  procedure Execute; override;
end;

procedure TTaskThread.Execute;
var
  i: integer;
begin
  Self.FreeOnTerminate := True;
  for i := 0 to 1000000000 do
    FloatToStr(i*1.31234);
end;

procedure TfrmMain.Button1Click(Sender: TObject);
var
  t1, t2, t3: TTaskThread;
begin
  t1 := TTaskThread.Create(True);
  t2 := TTaskThread.Create(True);
  t3 := TTaskThread.Create(True);
  t1.Start;
  t2.Start;
  t3.Start;
end;

这是一个'测试代码',其中CPU(通过性能监视器)最大值为25%(我有4个内核)。如果将FloatToStr行换成非字符串操作,例如Power(i,2),然后性能监视器显示预期的75%使用率。 (是的,有更好的方法来衡量这一点,但我认为这对于这个问题的范围是足够的)

我已经相当彻底地探讨了这个问题。问题的目的是以非常简单的形式提出问题的关键。

我在询问使用FloatToStr方法时的限制。并且要求有一个实现化身,它将允许更好地使用可用核心。

感谢。

5 个答案:

答案 0 :(得分:4)

我是其他人在评论中所说的。这是FastMM内存管理器无法扩展的Delphi的一个肮脏的小秘密。

由于可以替换内存管理器,您只需使用可扩展内存管理器替换FastMM即可。这是一个快速变化的领域。每隔几个月就会出现新的可扩展内存管理器。问题是很难编写正确的可扩展内存管理器。你准备信任什么? FastMM的优势之一就是它很强大。

最好不要更换内存管理器,而是更换内存管理器。简单地避免堆分配。找到一种方法来完成您的工作,需要重复调​​用来分配动态内存。即使您有可扩展的堆管理器,堆分配仍然会花费。

一旦您决定避免堆分配,下一个决定是使用什么而不是FloatToStr。根据我的经验,Delphi运行时库不提供太多支持。例如,我最近发现使用调用者提供的缓冲区没有很好的方法将整数转换为文本。因此,您可能需要滚动自己的转换函数。作为证明这一点的简单第一步,请尝试从sprintf调用msvcrt.dll。这将提供概念证明。

答案 1 :(得分:4)

如果您无法更改内存管理器(MM),唯一要做的就是避免在MM可能成为瓶颈的情况下使用它。

至于浮点到字符串的转换(Disclamer:我用Delphi XE测试了下面的代码)而不是

procedure Test1;
var
  i: integer;
  S: string;

begin
  for i := 0 to 10 do begin
    S:= FloatToStr(i*1.31234);
    Writeln(S);
  end;
end;

你可以使用

procedure Test2;
var
  i: integer;
  S: string;
  Value: Extended;

begin
  SetLength(S, 64);
  for i := 0 to 10 do begin
    Value:= i*1.31234;
    FillChar(PChar(S)^, 64, 0);
    FloatToText(PChar(S), Value, fvExtended, ffGeneral, 15, 0);
    Writeln(S);
  end;
end;

产生相同的结果,但不在循环内分配内存。

答案 2 :(得分:0)

并注意

function FloatToStr(Value: Extended): string; overload;
function FloatToStr(Value: Extended; const FormatSettings: TFormatSettings): string; overload;

FloatToStr的第一种形式不是线程安全的,因为它使用全局变量中包含的本地化信息。 FloatToStr的第二种形式是线程安全的,它指的是FormatSettings参数中包含的本地化信息。在调用FloatToStr的线程安全形式之前,必须使用本地化信息填充FormatSettings。要使用一组默认语言环境值填充FormatSettings,请调用GetLocaleFormatSettings。

答案 3 :(得分:0)

非常感谢您迄今为止的知识和帮助。根据您的建议,我试图以避免堆分配的方式编写等效的FloatToStr方法。取得一些成功。这绝不是一个可靠的简单实现,只是简单的简单概念证明,可以扩展到实现更令人满意的解决方案。

(还应注意使用XE6 64位)

实验结果/观察结果:

  • CPU使用率%与启动的线程数成正比 (即每个线程= 1个核心通过性能监视器最大化)。
  • 正如所料,随着更多线程的启动,每个人的性能都会有所下降(即执行任务所需的时间 - 请参阅代码)。

时间只是粗略的平均值

  • 8核3.3GHz - 1线程耗时4200ms。 6个主题各占5200毫秒。
  • 8核2.5GHz - 1线程耗时4800ms。 2 => 4800ms,4 => 5000ms,6 => 6300ms。

我没有计算总多线程运行的总时间。刚刚观察到CPU使用率%和测量的单个线程时间。

就我个人而言,我发现这实际上有点令人讨厌:)或许我做了一些可怕的错误?

当然有图书馆单位可以解决这些问题吗?

代码:

unit Main;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls,
  Generics.Collections,
  DateUtils;

type
  TfrmParallel = class(TForm)
    Button1: TButton;
    Memo1: TMemo;
    procedure Button1Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

  TTaskThread = class(TThread)
  private
    Fl: TList<double>;
  public
    procedure Add(l: TList<double>);
    procedure Execute; override;
  end;

var
  frmParallel: TfrmParallel;

implementation

{$R *.dfm}

{  TTaskThread  }

procedure TTaskThread.Add(l: TList<double>);
begin
  Fl := l;
end;

procedure TTaskThread.Execute;
var
  i, j: integer;
  s, xs: shortstring;

  FR: TFloatRec;
  V: double;
  Precision, D: integer;

  ZeroCount: integer;

  Start, Finish: TDateTime;

  procedure AppendByteToString(var Result: shortstring; const B: Byte);
  const
    A1 = '1';
    A2 = '2';
    A3 = '3';
    A4 = '4';
    A5 = '5';
    A6 = '6';
    A7 = '7';
    A8 = '8';
    A9 = '9';
    A0 = '0';
  begin
    if B = 49 then
      Result := Result + A1
    else if B = 50 then
      Result := Result + A2
    else if B = 51 then
      Result := Result + A3
    else if B = 52 then
      Result := Result + A4
    else if B = 53 then
      Result := Result + A5
    else if B = 54 then
      Result := Result + A6
    else if B = 55 then
      Result := Result + A7
    else if B = 56 then
      Result := Result + A8
    else if B = 57 then
      Result := Result + A9
    else
      Result := Result + A0;
  end;

  procedure AppendDP(var Result: shortstring);
  begin
    Result := Result + '.';
  end;

begin
  Precision := 9;
  D := 1000;
  Self.FreeOnTerminate := True;
  //
  Start := Now;
  for i := 0 to Fl.Count - 1 do
  begin
    V := Fl[i];   

//    //orignal way - just for testing
//    xs := shortstring(FloatToStrF(V, TFloatFormat.ffGeneral, Precision, D));

    //1. get float rec     
    FloatToDecimal(FR, V, TFloatValue.fvExtended, Precision, D);
    //2. check sign
    if FR.Negative then
      s := '-'
    else
      s := '';
    //2. handle negative exponent
    if FR.Exponent < 1 then
    begin
      AppendByteToString(s, 0);
      AppendDP(s);
      for j := 1 to Abs(FR.Exponent) do
        AppendByteToString(s, 0);
    end;      
    //3. count consecutive zeroes
    ZeroCount := 0;
    for j := Precision - 1 downto 0 do
    begin
      if (FR.Digits[j] > 48) and (FR.Digits[j] < 58) then
        Break;
      Inc(ZeroCount);
    end;
    //4. build string
    for j := 0 to Length(FR.Digits) - 1 do
    begin
      if j = Precision then
        Break;
      //cut off where there are only zeroes left up to precision
      if (j + ZeroCount) = Precision then
        Break;
      //insert decimal point - for positive exponent
      if (FR.Exponent > 0) and (j = FR.Exponent) then
        AppendDP(s);
      //append next digit
      AppendByteToString(s, FR.Digits[j]);
    end;      

//    //use just to test agreement with FloatToStrF
//    if s <> xs then
//      frmParallel.Memo1.Lines.Add(string(s + '|' + xs));

  end;
  Fl.Free;

  Finish := Now;
  //
  frmParallel.Memo1.Lines.Add(IntToStr(MillisecondsBetween(Start, Finish))); 
  //!YES LINE IS NOT THREAD SAFE!
end;

procedure TfrmParallel.Button1Click(Sender: TObject);
var
  i: integer;
  t: TTaskThread;
  l: TList<double>;
begin
  //pre generating the doubles is not required, is just a more useful test for me
  l := TList<double>.Create;
  for i := 0 to 10000000 do
    l.Add(Now/(-i-1)); //some double generation
  //
  t := TTaskThread.Create(True);
  t.Add(l);
  t.Start;
end;

end.

答案 4 :(得分:0)

FastMM4,默认情况下,在线程争用时,当一个线程无法获取数据访问权限时,被另一个线程锁定,调用Windows API函数Sleep(0),然后,如果锁定仍然不可用则通过调用进入循环每次检查锁定后休眠(1)。

每次调用Sleep(0)都会遇到上下文切换的昂贵代价,这可能是10000多个周期;它也会受到环3到0转换的成本,这可能是1000多个周期。关于Sleep(1) - 除了与Sleep(0)相关的成本之外 - 它还将执行延迟至少1毫秒,将控制权交给其他线程,如果没有线程等待物理CPU核心执行,使核心进入睡眠状态,有效降低CPU使用率和功耗。

这就是为什么,在你的情况下,CPU使用率从未达到100% - 因为FastMM4发布了Sleep(1)。

这种获取锁定的方式不是最佳的。

更好的方法是旋转锁定约5000 pause个指令,如果锁仍然忙,则调用SwitchToThread()API调用。如果pause不可用(在没有SSE2支持的非常旧的处理器上)或者SwitchToThread()API调用不可用(在非常旧的Windows版本上,在Windows 2000之前),最好的解决方案是使用EnterCriticalSection / LeaveCriticalSection,没有Sleep(1)关联的延迟,也非常有效地将CPU内核控制到其他线程。 我修改了FastMM4以使用一种新的方法来等待锁定:CriticalSections而不是Sleep()。使用这些选项,将永远不会使用Sleep(),而是使用EnterCriticalSection / LeaveCriticalSection。测试表明,使用CriticalSections而不是Sleep(在FastMM4之前默认使用)的方法在使用内存管理器的线程数与物理内核数相同或更高的情况下提供了显着的增益。在具有多个物理CPU和非统一内存访问(NUMA)的计算机上,增益更加明显。我已经实现了编译时选项来取消使用Sleep(InitialSleepTime)然后Sleep(AdditionalSleepTime)(或Sleep(0)和Sleep(1))的原始FastMM4方法,并用EnterCriticalSection / LeaveCriticalSection替换它们以节省宝贵的CPU周期被Sleep(0)浪费并提高速度(降低延迟),每次受Sleep(1)影响至少1毫秒,因为关键部分对CPU更友好,并且具有明显低于睡眠的延迟(1)

启用这些选项后,FastMM4-AVX会检查:

  • CPU是否支持SSE2,因此&#34;暂停&#34;指示,和
  • 操作系统是否具有SwitchToThread()API调用,以及

    在这种情况下使用&#34;暂停&#34;旋转循环5000次迭代然后SwitchToThread()而不是关键部分;如果CPU没有&#34;暂停&#34; instrcution或Windows没有SwitchToThread()API函数,它将使用EnterCriticalSection / LeaveCriticalSection。 我在https://github.com/maximmasiutin/FastMM4

  • 上提供了名为FastMM4-AVX的分支

以下是原始FastMM4版本4.992的比较,以及Delphi 10.2 Tokyo(优化发布)为Win64编译的默认选项,以及当前的FastMM4-AVX分支。在某些情况下,FastMM4-AVX分支的速度是原始FastMM4的两倍多。测试已经在两台不同的计算机上运行:一台在Xeon E6-2543v2下,带有2个CPU插槽,每个都有6个物理内核(12个逻辑线程) - 每个插槽只有5个物理内核可用于测试应用程序。另一项测试是在i7-7700K CPU下进行的。

使用&#34;多线程分配,使用和免费&#34;和#34; NexusDB&#34;来自FastCode Challenge Memory Manager测试套件的测试用例,修改为在64位下运行。

                     Xeon E6-2543v2 2*CPU     i7-7700K CPU
                    (allocated 20 logical  (allocated 8 logical
                     threads, 10 physical   threads, 4 physical
                     cores, NUMA)           cores)

                    Orig.  AVX-br.  Ratio   Orig.  AVX-br. Ratio
                    ------  -----  ------   -----  -----  ------
02-threads realloc   96552  59951  62.09%   65213  49471  75.86%
04-threads realloc   97998  39494  40.30%   64402  47714  74.09%
08-threads realloc   98325  33743  34.32%   64796  58754  90.68%
16-threads realloc  116708  45855  39.29%   71457  60173  84.21%
16-threads realloc  116273  45161  38.84%   70722  60293  85.25%
31-threads realloc  122528  53616  43.76%   70939  62962  88.76%
64-threads realloc  137661  54330  39.47%   73696  64824  87.96%
NexusDB 02 threads  122846  90380  73.72%   79479  66153  83.23%
NexusDB 04 threads  122131  53103  43.77%   69183  43001  62.16%
NexusDB 08 threads  124419  40914  32.88%   64977  33609  51.72%
NexusDB 12 threads  181239  55818  30.80%   83983  44658  53.18%
NexusDB 16 threads  135211  62044  43.61%   59917  32463  54.18%
NexusDB 31 threads  134815  48132  33.46%   54686  31184  57.02%
NexusDB 64 threads  187094  57672  30.25%   63089  41955  66.50%

调用FloatToStr的代​​码没问题,因为它使用内存管理器分配结果字符串,然后重新分配它等等。更好的想法是明确地释放它,例如:

procedure TTaskThread.Execute;
var
  i: integer;
  s: string;
begin
  for i := 0 to 1000000000 do
  begin
    s := FloatToStr(i*1.31234);
    Finalize(s);
  end;
end;

您可以在http://fastcode.sourceforge.net/

的FastCode挑战测试套件中找到更好的内存管理器测试