【问题标题】:Delphi loop speed questionDelphi循环速度问题
【发布时间】:2010-01-20 04:06:24
【问题描述】:

有没有更快的方法?我基本上需要一次将 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
          //

      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.
         sleep(1);  //Without this it doesn't process the cancel button.
         if cancel then Break; 
         slist2.Add(slist1.Strings[i]+ch+ch2);
        end;
    end;
end;
insertsingle(slist2,queuebox);
freeandnil(slist1);
freeandnil(slist2);

结束;

感谢您的帮助

【问题讨论】:

  • 你能举个例子吗?我不明白我怎么能只用一个循环来做到这一点。必须做AA,AB,AC ... ZZ
  • 请从可以编译的示例代码开始。对,您的代码取决于“queuebox”、“cancel”和“insertsingle”,但它们不存在。我猜 queuebox 是一个列表框,但它是吗?

标签: delphi loops for-loop performance


【解决方案1】:

如果您希望在循环期间处理事件,例如单击取消按钮,调用Application.ProcessMessages 就足够了。如果你经常打电话但不太经常打电话,例如每秒 50 次,那么您的应用程序将保持对取消按钮的响应,而不会减慢太多。如果没有任何消息要处理,Application.ProcessMessages 会很快返回。

这种技术适用于您希望用户等待的相对较短的计算(几秒钟)。对于长计算,后台线程更合适。然后,您的应用程序可以保持完全响应,尤其是在用户拥有多核 CPU 的情况下。

在主线程中调用Sleep 不允许您的应用程序处理事件。它允许其他应用程序做一些事情。调用 Sleep 确实会使您的应用程序(实际上是调用线程)在请求的时间量或线程时间片的剩余时间内休眠,以较大者为准。

【讨论】:

    【解决方案2】:

    您的代码有几个明显的问题。

    首先,您会浪费大量 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 //sleep and process messages here 之类的东西。 Craig 建议将此任务移至工作线程也会有所帮助,但前提是您对线程有足够的了解以使其正确。它们可能很棘手。

    【讨论】:

    • 感谢您的信息,不,我对 Threads 的了解还不够,无法正确使用它。:P 我是 Delphi 编程的新手。您对我在哪里可以找到有关将其保存为字符串资源的更多信息有任何建议吗?
    • 由于您是新手,因此不必费心将其放入资源中——尤其是因为这可能不会更快。您在不计算时保存的任何内容都可能在 I/O 加载时返回。
    • 除非我弄错了,ch mod 100 = 0 只会为真一次! ch mod 10 = 0 可能会更好..
    【解决方案3】:

    好的。我试图优化你的代码。对于最终测试,需要一些测试数据。

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

    • 注释掉“cancel”和“”的代码
    • 给类型和变量一个更有意义的名字
    • 使用 Delphi 使用的名称(“应用程序”而不是“应用程序”等)使其可读
    • 将一些逻辑移到“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;
    

    --杰罗恩

    【讨论】:

    • 感谢 cmets 和代码示例...我打算在早上尝试一下...我不知道 TStringBuilder,我刚开始使用 TStringList 并感觉像我不会破坏任何东西..:P
    【解决方案4】:

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

    根据我的经验,通过使用更少的 ProcessMessages(); Sleep(1); 语句,您将获得非常很大的改进,正如其他答案中所建议的那样。

    尝试将它移到第一个 for 循环的正下方,看看你得到了什么改进。

    【讨论】:

    • +1 简短而准确。只有在线程中运行它的可能性是缺少恕我直言。也许你可以编辑它。
    【解决方案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;
    

    【讨论】:

    • 您可以通过在 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 上测试。
    • 错字,SourceList[I] 应该是 FSource[I]。
    • @LURD - 不错,您基本上是在预先设置字符串大小,防止它不得不调整大小。毫无疑问,这会更快。
    【解决方案6】:

    我会看看你是否可以按照评论在一个循环中完成。还可以尝试在线程中执行此操作,这样您就可以在不阻塞 UI 的情况下消除 Application.ProcessMessages 和 Sleep 调用。

    【讨论】:

    • +1 用于建议一个线程用于保持 GUI 响应的长期任务。
    【解决方案7】:

    我知道这并不能具体回答您的问题,但如果您对 Delphi 算法感兴趣,Julian Bucknall(DevExpress 的首席技术官)撰写了权威的 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

    【讨论】:

      【解决方案8】:

      试试这个示例代码 - 希望这会有所帮助(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;    
      

      --莱因哈德

      【讨论】:

      • 除非我错过了什么,这是设置更多,以将 676 个选项添加到一个起始字符串 VS 数百/数千个列表。
      • 是的,你是对的,如果你将 AA 到 ZZ 添加到列表中,那么只有 676 个可能的值 (26 * 26)。如果你需要更多让我们说第三个字符 AAA 到 ZZZ (26 * 26 * 26) 你最多可以处理 17576 还是你想再次从 AA 开始?。
      • 我想我理解错了你的问题...更改了代码
      【解决方案9】:

      为此目的使用Delphi backgroundworker组件可以比线程更好。它是一个简单且基于事件的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;
      

      【讨论】:

        【解决方案10】:

        如果您正在寻找纯粹的速度,只需将代码展开到单个循环中并将每一行写为单独的分配。您可以编写一个程序来自动为您编写这些行,然后将它们复制并粘贴到您的代码中。这基本上是可能的最快方法。还要关闭上面提到的所有更新。

        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') 行,这样您就不必自己编写它们了.. :) 希望我正确理解了这个问题。跨度>
        • -1 以获得完全无益的答案。这具有错误的优化尝试(将循环展开为 26 * 26 复杂语句的序列),而未能应用明显的优化尝试(仅访问一次 slist1.Strings[i])。如果生成的代码更大,由于较低的局部性和较高的缓存失效,循环展开在现代系统上肯定会适得其反。
        猜你喜欢
        • 2014-01-25
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2022-01-26
        • 1970-01-01
        • 2020-04-28
        • 2012-09-23
        • 1970-01-01
        相关资源
        最近更新 更多