目标是在单个Delphi应用程序中将浮点数转换为字符串,以实现可用内核的完全使用。我认为这个问题适用于字符串的一般处理。然而在我的例子中,我特意使用了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方法时的限制。并且要求有一个实现化身,它将允许更好地使用可用核心。
感谢。
答案 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使用率%和测量的单个线程时间。
就我个人而言,我发现这实际上有点令人讨厌:)或许我做了一些可怕的错误?
当然有图书馆单位可以解决这些问题吗?
代码:
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会检查:
操作系统是否具有SwitchToThread()API调用,以及
在这种情况下使用&#34;暂停&#34;旋转循环5000次迭代然后SwitchToThread()而不是关键部分;如果CPU没有&#34;暂停&#34; instrcution或Windows没有SwitchToThread()API函数,它将使用EnterCriticalSection / LeaveCriticalSection。 我在https://github.com/maximmasiutin/FastMM4
以下是原始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;
的FastCode挑战测试套件中找到更好的内存管理器测试