Delphi循环速度问题

5

有更快的方法吗?我需要一次性为数千个记录添加AA-ZZ。

仅列出35个项目就需要相当长的时间,更不用说一千个了。


procedure Tmainform.btnSeederClick(Sender: TObject);
var
  ch,ch2:char;
  i:integer;
  slist1, slist2:TStrings;
begin
  slist1:= TStringList.Create;
  slist2:= TStringList.Create;
  slist1.Text :=queuebox.Items.Text;
  for ch := 'a' to 'z' do
    begin
      for ch2 := 'a' to 'z' do
        begin
          //

结束;

感谢任何帮助


你能给我一个例子吗?我不知道如何只用一个循环来实现它。它必须做到AA,AB,AC...ZZ。 - Brad
请先提供一个可以编译的示例代码。你的代码依赖于“queuebox”、“cancel”和“insertsingle”,但它们并不存在。我猜“queuebox”是一个列表框,但它真的是吗? - Jeroen Wiert Pluimers
10个回答

14

你的代码存在一些明显的问题。

首先,你浪费了很多CPU时间在重复计算相同的值上。AA..ZZ的值不会改变,因此没有必要重复构建它们。尝试这样做:创建第三个TStringList。使用双重循环遍历所有可能的AA..ZZ排列,并将其填充到第三个列表中。完成后,循环并将预先计算好的字符串列表与slist1中的值合并。你应该可以从中看到一个相当大的提升。

(或者,如果时间确实非常紧迫,编写一个小程序来计算排列列表并将其保存到文本文件中,然后将其编译为字符串资源并在运行时加载。)

其次,这可能是导致问题的主要原因,你不应该在最内层循环中使用ProcessMessages和Sleep调用。Sleep(1);听起来像是“睡眠1毫秒”,但Windows不提供那种精度。你最终得到的是“睡眠至少1毫秒”。它会释放CPU直到Windows回到它,通常需要大约16毫秒左右。因此,你会在一个非常紧密的循环中增加16毫秒(加上ProcessMessages所需的时间)的延迟,而这个循环可能只需要几微秒就能执行完其余的代码。

如果你需要类似的东西来保持UI响应,请将它放在最外层循环中,而不是内部循环中,而且可能甚至不需要每次迭代都运行它。尝试像这样: if ch mod 100 = 0 then //在此处睡眠并处理消息。Craig建议将此任务移动到工作线程中也会有所帮助,但前提是你了解足够多关于线程的知识才能做到正确。它们可能很棘手。


谢谢提供的信息,我对线程不够了解,无法正确地处理它..: P 我最多只是个Delphi编程的新手。请问您有任何建议在哪里可以找到更多保存为字符串资源的信息? - Brad
1
既然你是新手,不要费心将它放到资源中--特别是因为那也可能不会更快。你在节省计算时所获得的一切,可能会在输入/输出加载中损失。 - Loren Pechtel
除非我错了,ch mod 100 = 0 只会在一次时成立! ch mod 10 = 0 可能更好。 - Blorgbeard

11

你应该用slist2.BeginUpdate()slist2.EndUpdate()包围你的代码,以防止TStringList进行额外处理。

根据我的经验,使用更少的ProcessMessages(); Sleep(1);语句会得到非常大的提高,正如其他答案中建议的那样。

尝试将其移动到第一个for循环的下方,看看你能得到什么改进。


1
简短而精确。仅缺少在线程中运行的可能性,我认为。也许你可以编辑它。 - jpfollenius

5

使用辅助线程处理繁重工作的示例

请注意,对于您提到的35个项目,启动另一个线程真的不值得。但如果有几千个项目,情况就不同了。在我的台式电脑上处理10,000个项目需要10秒钟。

多线程的一些好处:

  • 主线程保持响应。
  • 可以随时停止计算。

当然还有一些缺点:

  • 必须小心(在当前实现中)不要在种子运行时搞乱传递的字符串列表。
  • 多线程增加了复杂性,并且是难以找到错误源的原因。

将下面的代码粘贴到您喜欢的编辑器中,然后您就可以开始了。

procedure TForm1.btnStartClick(Sender: TObject);
var
  I: Integer;
begin
  //***** Fill the sourcelist
  FSource := TStringList.Create;
  FDestination := TStringList.Create;
  for I := 0 to 9999 do
    FSource.Add(Format('Test%0:d', [I]));

  //***** Create and fire Thread
  FSeeder := TSeeder.Create(FSource, FDestination);
  FSeeder.OnTerminate := DoSeederDone;
  FSeeder.Resume;
end;

procedure TForm1.btnStopClick(Sender: TObject);
begin
  if Assigned(FSeeder) then
    FSeeder.Terminate;
end;

procedure TForm1.DoSeederDone(Sender: TObject);
var
  I, step: Integer;
begin
  I := 0;
  step := 0;
  while I < FDestination.Count do
  begin
    //***** Don't show every item. OutputDebugString is pretty slow.
    OutputDebugString(PChar(FDestination[I]));
    Inc(step);
    Inc(I, step);
  end;
  FSource.Free;
  FDestination.Free;
end;

{ TSeeder }

constructor TSeeder.Create(const source, destination: TStringList);
begin
  //***** Create a suspended, automatically freed Thread object.
  Assert(Assigned(source));
  Assert(Assigned(destination));
  Assert(destination.Count = 0);
  inherited Create(True);
  FreeOnTerminate := True; //***** Triggers the OnTerminate event
  FSource := source;
  FDestination := destination;
end;

procedure TSeeder.Execute;
var
  I, J: Integer;
  AString: string;
begin
  FDestination.BeginUpdate;
  try
    FDestination.Capacity := FSource.Count * 26 * 26;
    for I := 0 to Pred(FSource.Count) do
    begin
      AString := FSource[I];
      for J := 0 to Pred(26 * 26) do
      begin
        FDestination.Add(AString + Char(J div 26 + $41) + Char(J mod 26 + $41));
        if Terminated then Exit;
      end;
    end;
  finally
    FDestination.EndUpdate;
  end;
end;

1
通过在TSeeder.Execute中执行以下操作,您可以将时间缩短50%:AString := SourceList[I] + ' '; L:= Length(AString); for J := 0 to Pred(26 * 26) do begin AString[L-1]:= Char(J div 26 + $41); AString[L]:= Char(J mod 26 + $41); FDestination.AddObject(AString, Nil); end;在10000个FSource上进行了测试。 - LU RD
笔误,SourceList[I] 应该是 FSource[I]。 - LU RD
@LURD - 很好,你基本上是预先设置字符串大小,防止它需要重新调整大小。毫无疑问,那样会更快。 - Lieven Keersmaekers

4

好的。我已经尝试优化了你的代码。为了进行最终测试,需要一些测试数据。

我所做的事情(包括Mason的大部分想法):

  • 注释掉关于“cancel”和“
  • 给类型和变量起一个更有意义的名称
  • 使用Delphi使用的名称(例如,“Application”而不是“application”等)使其易读
  • 将一些逻辑移入“KeepUIGoing”
  • 将后缀的计算从主循环中移出到初始化循环中
  • 使其可选地使用TStringBuilder(比TStringList快得多,并且自Delphi 2009以来可用)

下面是修改后的代码,请告诉我它是否适用于你。

procedure TForm2.Button1Click(Sender: TObject);
{$define UseStringBuilder}
  procedure KeepUIGoing(SourceListIndex: Integer);
  begin
    if SourceListIndex mod 100 = 0 then
    begin
      Application.ProcessMessages;
      // so it doesn't freeze the application in long loops.  Not 100% sure where this should be placed, if at all.
      Sleep(1);
    end;
  end;
const
  First = 'a';
  Last = 'z';
type
  TRange = First .. Last;
  TSuffixes = array [TRange, TRange] of string;
var
  OuterIndex, InnerIndex: Char;
  SourceListIndex: Integer;
  SourceList, TargetList: TStrings;
  Suffixes: TSuffixes;
  NewLine: string;
{$ifdef UseStringBuilder}
  TargetStringBuilder: TStringBuilder; // could be way faster than TStringList
{$endif UseStringBuilder}
begin
  for OuterIndex := First to Last do
    for InnerIndex := First to Last do
      Suffixes[OuterIndex, InnerIndex] := OuterIndex + InnerIndex;

  SourceList := TStringList.Create;
  TargetList := TStringList.Create;
{$ifdef UseStringBuilder}
  TargetStringBuilder := TStringBuilder.Create();
{$endif UseStringBuilder}
  try
    SourceList.Text := queuebox.Items.Text;
    for OuterIndex := First to Last do
    begin
      for InnerIndex := First to Last do
      begin
        for SourceListIndex := 0 to SourceList.Count - 1 do
        begin
          KeepUIGoing(SourceListIndex);
          // if cancel then
          // Break;
          NewLine := SourceList.Strings[SourceListIndex] + Suffixes[OuterIndex, InnerIndex];
{$ifdef UseStringBuilder}
          TargetStringBuilder.AppendLine(NewLine);
{$else}
          TargetList.Add(NewLine);
{$endif UseStringBuilder}
        end;
      end;
    end;
{$ifdef UseStringBuilder}
    TargetList.Text := TargetStringBuilder.ToString();
{$endif UseStringBuilder}
    // insertsingle(TargetList, queuebox);
  finally
{$ifdef UseStringBuilder}
    FreeAndNil(TargetStringBuilder);
{$endif UseStringBuilder}
    FreeAndNil(SourceList);
    FreeAndNil(TargetList);
  end;
end;

--jeroen


感谢您的评论和代码示例...我明天会尝试一下...我不知道TStringBuilder,我刚开始使用TStringList,感觉自己不会弄坏什么.. :P - Brad

4

我建议您尝试根据评论在一个循环中完成操作。同时,尝试使用线程来消除Application.ProcessMessages和Sleep调用,以避免阻塞用户界面。


为长时间运行的任务建议一个线程,以保持GUI响应。 - jpfollenius

1

尝试这个示例代码 - 希望能有所帮助(Delphi 5 Ent./WinXP)

procedure TForm1.Button1Click(Sender: TObject);
var
   i,k: Integer;
   sourceList, destList: TStringList;
   ch1, ch2: char;
begin
   destList := TStringList.Create;
   sourceList := TStringList.Create;

   //some sample data but I guess your list will have 1000+
   //entries?
   sourceList.Add('Element1');
   sourceList.Add('Element2');
   sourceList.Add('Element3');

   try
      i := 0;
      while i < (26*26) do
      begin
         if (i mod 100) = 0 then
            Application.ProcessMessages;

         ch1 := char(65 + (i div 26));
         ch2 := char(65 + (i mod 26));

         for k := 0 to sourceList.Count -1 do
            destList.Add(Format('%s-%s%s', [sourceList.Strings[k], ch1, ch2]));
         Inc(i);
      end;

      Memo1.Lines.AddStrings(destList);
   finally
      FreeAndNil(destList);
      FreeAndNil(sourceList);
   end;
end;    

--Reinhard


除非我漏掉了些什么,否则这个设置更适合将676个选项添加到一个起始字符串中,而不是成百上千的清单。 - Brad
是的,你说得对。如果将AA到ZZ添加到列表中,则只有676个可能的值(26 * 26)。如果您需要更多,比如第三个字符AAA到ZZZ(26 * 26 * 26),则最多可以处理17576个,或者您想再次从AA开始吗? - macf00bar
我觉得我误解了你的问题...已经修改了代码。 - macf00bar

1

使用 Delphi backgroundworker 组件比线程更好。它是一个易于使用且基于事件的组件。背景工作者的特点(同时使用线程):

  • 使用基于事件的代码,无需创建类
  • 添加进度处理功能

示例代码:

procedure TForm2.FormCreate(Sender: TObject);
var
  I: Integer;
begin
  FSource := TStringList.Create;
  FDestination := TStringList.Create;

end;
procedure TForm2.Button1Click(Sender: TObject);
var
  I: Integer;
begin
  try
    FSource.BeginUpdate;
    FSource.Clear;
    for I := 0 to 9999 do
      FSource.Add(Format('Test%0:d', [I]));
    BackgroundWorker1.Execute;
  finally
    FSource.EndUpdate;
  end;

end;



procedure TForm2.StopButtonClick(Sender: TObject);
begin
  BackgroundWorker1.Cancel;
end;



procedure TForm2.FormDestroy(Sender: TObject);
begin
 FreeAndNil(FSource);
 FreeAndNil(FDestination);
end;


procedure TForm2.BackgroundWorker1Work(Worker: TBackgroundWorker);
var
  I, J: Integer;
  AString: string;
begin
  FDestination.BeginUpdate;
  try
    FDestination.Capacity := FSource.Count * 26 * 26;
    for I := 0 to Pred(FSource.Count) do
    begin
      AString := FSource[I];
      for J := 0 to Pred(26 * 26) do
      begin
        FDestination.Add(AString + Char(J div 26 + $41) + Char(J mod 26 + $41));
        if Worker.CancellationPending then
          Exit;
      end;
      if I mod 10 = 0 then
        TThread.Sleep(1);
      Worker.ReportProgress((I * 100) div FSource.Count);
    end;
    Worker.ReportProgress(100); // completed

  finally
    FDestination.EndUpdate;
  end;
end;

procedure TForm2.BackgroundWorker1WorkProgress(Worker: TBackgroundWorker;
  PercentDone: Integer);
begin
  ProgressBar1.Position := PercentDone;
end;

1

如果你希望在循环期间处理事件,例如单击取消按钮,调用 Application.ProcessMessages 就足够了。如果您定期调用它但不要太频繁,例如每秒50次,则您的应用程序将保持对取消按钮的响应而不会减慢过多速度。如果没有任何要处理的消息,则Application.ProcessMessages将相当快地返回。

这种技术适用于相对较短的计算(几秒钟),您预计用户会等待。对于长时间的计算,后台线程更加合适。然后,您的应用程序就可以保持完全响应,特别是如果用户拥有多核CPU。

在主线程中调用 Sleep 不允许您的应用程序处理事件。它允许其他应用程序执行某些操作。实际上,调用 Sleep 会将您的应用程序(实际上是调用线程)置于请求的时间或线程时间片剩余时间的睡眠状态中,以较大者为准。


1

我知道这并没有直接回答你的问题,但如果你对 Delphi 算法感兴趣,Julian Bucknall(DevExpress 的 CTO)写了一本权威的 Delphi 算法书。

Tomes of Delphi: Algorithms and Data Structures

  • 第 1 章:什么是算法?
  • 第 2 章:数组
  • 第 3 章:链表、栈和队列
  • 第 4 章:搜索
  • 第 5 章:排序
  • 第 6 章:随机化算法
  • 第 7 章:哈希和哈希表
  • 第 8 章:二叉树
  • 第 9 章:优先队列和堆排序
  • 第 10 章:状态机和正则表达式
  • 第 11 章:数据压缩
  • 第 12 章:高级主题

您还可以获得他的EZDSL(易于使用的数据结构库)适用于Delphi 2009Delphi 6-2007


-1
如果您在寻找纯速度,只需将代码展开为单个循环,并将每行编写为单独的赋值即可。您可以编写一个程序自动为您编写这些行,然后将它们复制并粘贴到您的代码中。这基本上是最快的方法。还要像上面提到的那样关闭所有更新。
procedure Tmainform.btnSeederClick(Sender: TObject);
var
  ch,ch2:char;
  i:integer;
  slist1, slist2:TStrings;
begin
  slist1:= TStringList.Create;
  slist2:= TStringList.Create;

  slist1.Text :=queuebox.Items.Text;

  slist2.BeginUpdate() 
     for I := 0 to slist1.Count - 1 do
        begin
        application.ProcessMessages; // so it doesn't freeze the application in long loops.  Not 100% sure where this should be placed, if at all.
         if cancel then Break; 
         slist2.Add(slist1.Strings[i]+'AA');
         slist2.Add(slist1.Strings[i]+'AB');
         slist2.Add(slist1.Strings[i]+'AC');
         ...
         slist2.Add(slist1.Strings[i]+'ZZ');
        end;
slist2.EndUpdate()
insertsingle(slist2,queuebox);
freeandnil(slist1);
freeandnil(slist2);
end;

哦,你可以写一个小程序来创建slist2.add(...'AA')这样的行,这样你就不必自己写了.. :) 希望我正确理解了问题。 - General Jackson Tackett
1
这段代码存在错误的优化尝试(将一个循环展开为26 * 26个复杂语句的序列),却未能应用明显的优化方法(仅访问slist1.Strings[i]一次)。如果展开后的代码更大,由于局部性较低和高缓存失效率,现代系统上循环展开肯定是适得其反的。 - mghie

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接