【问题标题】:Quick padding of a string in DelphiDelphi中字符串的快速填充
【发布时间】:2010-12-13 08:32:24
【问题描述】:

我试图加快应用程序中的某个例程,而我的分析器 AQTime 将一种方法特别确定为瓶颈。该方法已经存在多年,并且是“杂项”单元的一部分:

function cwLeftPad(aString:string; aCharCount:integer; aChar:char): string;
var
  i,vLength:integer;
begin
  Result := aString;
  vLength := Length(aString);
  for I := (vLength + 1) to aCharCount do    
    Result := aChar + Result;
end;

在我正在优化的程序部分中,该方法被调用了大约 35k 次,它花费了惊人的 56% 的执行时间!

很容易看出左填充字符串是一种可怕的方式,所以我将其替换为

function cwLeftPad(const aString:string; aCharCount:integer; aChar:char): string; 
begin
  Result := StringOfChar(aChar, aCharCount-length(aString))+aString;
end;

这给了很大的推动作用。总运行时间从 10.2 秒到 5.4 秒。惊人的!但是,cwLeftPad 仍然占总运行时间的 13% 左右。有没有一种简单的方法可以进一步优化这种方法?

【问题讨论】:

  • 您是否有关于您的函数所涉及的每个 RTL 函数花费了多少时间的数据?比如说,分配内存和复制字符的百分比是多少?
  • 您使用的是 D2009 或更高版本,即您使用的是 string=ansistring 还是 unicode 字符串?
  • 这个函数的典型输入是什么?如果您有一组有限的真实世界输入,那么可以以一种在一般情况下可能较慢但对您来说更快的方式来调整算法。 Wodzu 有一个极端的例子。
  • 通常 5-15 个字符的字符串被填充到 20-50 个字符。
  • 每次填充字符都不一样,是一个填充字符还是多个填充字符,例如:'.',0','#'。

标签: algorithm delphi optimization string


【解决方案1】:

您的新函数涉及三个字符串,即输入、StringOfChar 的结果和函数结果。当您的函数返回时,其中一个会被销毁。您可以分两步完成,不会破坏或重新分配任何内容。

  1. 分配所需总长度的字符串。
  2. 用您的填充字符填充它的第一部分。
  3. 用输入字符串填充其余部分。

这是一个例子:

function cwLeftPad(const aString: AnsiString; aCharCount: Integer; aChar: AnsiChar): AnsiString;
var
  PadCount: Integer;
begin
  PadCount := ACharCount - Length(AString);
  if PadCount > 0 then begin
    SetLength(Result, ACharCount);
    FillChar(Result[1], PadCount, AChar);
    Move(AString[1], Result[PadCount + 1], Length(AString));
  end else
    Result := AString;
end;

我不知道 Delphi 2009 及更高版本是否提供了基于双字节 Char 的 FillChar 等效项,如果提供,我不知道它叫什么,所以我已将函数的签名更改为显式使用 AnsiString。如果您需要 WideString 或 UnicodeString,则必须找到处理两字节字符的 FillChar 替换。 (FillChar 在 Delphi 2009 中的名称令人困惑,因为它不处理完整大小的 Char 值。)

要考虑的另一件事是,您是否真的需要如此频繁地调用该函数。最快的代码是永远不会运行的代码。

【讨论】:

  • Afaik D2009 没有。 FPC提供fillword/dword/qword
  • 使它成为一个 VAR 过程而不是一个函数可能会使它稍微快一点(如果字符串的引用计数为 1 并且已分配,并且可以放大/缩小,则字符串分配更便宜)。可能会以易于使用为代价。
  • Marco,返回字符串的函数无论如何都会被编译器转换为 var 过程。 (请参阅困惑的开发人员的许多报告,其中 Result 保存先前调用的值,而不是像普通局部变量那样是空字符串。)
  • 在 Delphi 2009 中,FillChar 不起作用。它需要一个字节数,并希望填充字符是一个单字节字符,并将用它填充每个字节。 Delphi 2009 对 FillChar 的帮助建议使用 StringOfChar 代替,它在系统单元中并用汇编程序编写,因此它显然已经过优化,应该可以解决问题。
  • FillChar 将在所有版本的 Delphi 中对我的函数正常工作,因为正如我所指出的,我的函数使用 AnsiString。对于 UnicodeString,找到一个 FillWord 或 FillWideChar 函数;例如,在 JclWideFormat.pas 中有一个。
【解决方案2】:

另一个想法 - 如果这是 Delphi 2009 或 2010,请在项目、选项、Delphi 编译器、编译、代码生成中禁用“字符串格式检查”。

【讨论】:

    【解决方案3】:

    StringOfChar 的速度非常快,我怀疑您是否可以大大改进此代码。不过,试试这个,也许它更快:

    function cwLeftPad(aString:string; aCharCount:integer; aChar:char): string;
    var
      i,vLength:integer;
      origSize: integer;
    begin
      Result := aString;
      origSize := Length(Result);
      if aCharCount <= origSize then
        Exit;
      SetLength(Result, aCharCount);
      Move(Result[1], Result[aCharCount-origSize+1], origSize * SizeOf(char));
      for i := 1 to aCharCount - origSize do
        Result[i] := aChar;
    end;
    

    编辑:我做了一些测试,我的功能比你改进的 cwLeftPad 慢。但我发现了别的东西——除非你在 PC XT 上运行或格式化千兆字节字符串,否则你的 CPU 不可能需要 5 秒来执行 35k cwLeftPad 函数。

    我用这个简单的代码进行了测试

    for i := 1 to 35000 do begin
      a := 'abcd1234';
      b := cwLeftPad(a, 73, '.');
    end;
    

    你原来的 cwLeftPad 有 255 毫秒,改进后的 cwLeftPad 有 8 毫秒,我的版本有 16 毫秒。

    【讨论】:

    • 运行时间为 5.4 秒。字符串填充函数是其中的 13%。不过,这是 0.7 秒,如果您看到 0.008,这仍然相当高。
    • 大概8ms是执行时间内所有cwLeftPad调用的累计时间
    • 8 毫秒是 35.000 次字符串分配(来自一个常数 - 我想非常快)和 35.000 次 cwLeftPad 调用。
    • gabr,我在做一个小测试项目时遇到了和你一样的事情。字符串甚至填充到更短的长度(25 个字符),这将使这两种方法更加平等。我开始相信分析器在跟我开玩笑:-) 可能澄清的一件事是问题中的数字来自调试版本,我习惯性地关闭代码生成优化。当我对旧方法进行优化重复测试时,大约需要总运行时间的 20%,而新版本只需要略多于总时间的 2%。
    • sveinbringsli:大警告!不要相信 AQTime 进行微优化。见:stackoverflow.com/questions/332948/…
    【解决方案4】:

    你现在每次都调用 StringOfChar。当然这个方法会检查它是否有事情要做,如果长度足够小就跳出,但是对 StringOfChar 的调用可能很耗时,因为在内部它会在跳出之前再调用一次。

    所以我的第一个想法就是没事就自己跳出来:

    function cwLeftPad(const aString: string; aCharCount: Integer; aChar: Char;): string;
    var
      l_restLength: Integer;
    begin
      Result  := aString;
      l_restLength := aCharCount - Length(aString);
      if (l_restLength < 1) then
        exit;
    
      Result := StringOfChar(aChar, l_restLength) + aString;
    end;
    

    【讨论】:

    • 您可以通过对来自系统单元的 StringOfChar 例程的副本使用内联指令来绕过调用的开销。或者你如果你懂一点汇编,你可以自己把汇编直接插入到 cwLeftPad 函数中,没有 PUSH 和 POP 语句的开销。
    【解决方案5】:

    您可以使用查找数组进一步加快此例程。

    当然,这取决于您的要求。如果你不介意浪费一些内存... 我猜该函数被调用了 35 k 次,但它没有 35000 个不同的填充长度和许多不同的字符。

    因此,如果您知道(或者您能够以某种快速方式估计)填充范围和填充字符,您可以构建一个包含这些参数的二维数组。 为简单起见,我假设您有 10 种不同的填充长度,并且您使用一个字符 - '.' 进行填充,因此在示例中它将是一维数组。

    你可以这样实现它:

    type
      TPaddingArray = array of String;
    
    var
      PaddingArray: TPaddingArray;
      TestString: String;
    
    function cwLeftPad4(const aString:string; const aCharCount:integer; const aChar:char; var anArray: TPaddingArray ): string;
    begin
      Result := anArray[aCharCount-length(aString)] + aString;
    end;
    
    begin
      //fill up the array
      SetLength(StrArray, 10);
      PaddingArray[0] := '';
      PaddingArray[1] := '.';
      PaddingArray[2] := '..';
      PaddingArray[3] := '...';
      PaddingArray[4] := '....';
      PaddingArray[5] := '.....';
      PaddingArray[6] := '......';
      PaddingArray[7] := '.......';
      PaddingArray[8] := '........';
      PaddingArray[9] := '.........';
    
      //and you call it..
      TestString := cwLeftPad4('Some string', 20, '.', PaddingArray);
    end;
    

    以下是基准测试结果:

    Time1 - oryginal cwLeftPad          : 27,0043604142394 ms.
    Time2 - your modyfication cwLeftPad : 9,25971967336897 ms.
    Time3 - Rob Kennedy's version       : 7,64538131122457 ms.
    Time4 - cwLeftPad4                  : 6,6417059620664 ms.
    

    更新的基准:

    Time1 - oryginal cwLeftPad          : 26,8360194218451 ms.
    Time2 - your modyfication cwLeftPad : 9,69653117046119 ms.
    Time3 - Rob Kennedy's version       : 7,71149259179622 ms.
    Time4 - cwLeftPad4                  : 6,58248533610693 ms.
    Time5 - JosephStyons's version      : 8,76641780969192 ms.
    

    问题是:值得麻烦吗?;-)

    【讨论】:

    • 如果我想用零而不是点填充怎么办? :-)
    • 正如我在回答中所说,如果您知道要填充哪些字符/字符,则可以为其构建特定的数组。您是否需要允许多个字符的更详细的示例? :)
    • 你是对的,我很抱歉。我没有很好地阅读您的介绍,只是代码。但无论如何,为什么你把 aChar 参数留在函数中呢? :-)
    • 啊!谢谢@sveinbringsli 我没有注意到:)
    • 仅供参考:实际上这不是线程安全的函数和方法。所以我投票支持 Rob 的答案,即使这种方法可能是安全的。 1ms 的加速并不重要。也缺少任何输入参数检查和对数组的不安全访问。
    【解决方案6】:

    使用 StringOfChar 分配一个全新的字符串(字符串长度和填充)可能会更快,然后使用 move 将现有文本复制到它的后面。
    我的想法是您在上面创建了两个新字符串(一个带有 FillChar,一个带有加号)。这需要两次内存分配和字符串伪对象的构造。这会很慢。浪费几个 CPU 周期来做一些冗余填充以避免额外的内存操作可能会更快。
    如果您分配内存空间然后执行 FillChar 和 Move,它可能会更快,但额外的 fn 调用可能会减慢速度。
    这些事情往往是反复试验!

    【讨论】:

    • 没有“额外的函数调用”; StringOfChar 无论如何都会调用 FillChar。
    • 很公平!所以 SetLength(), Fillchar(left hand side), Move(right hand side) 应该更快。 TBH 自从我编写 Delphi 以来已经有几年了,我根本不记得 StringOfChar fn。我现在注意到,初始字符串是按值传递的。在 Delphi 中,IIRC(我可能不会)这意味着它是克隆的。通过引用传递它可能是值得的。人们可能会觉得编码标准会为此打死你,但它应该更快。
    • @sinibar - 通过 ref 传递:是的,aString 应该作为 const 传递。否则会有不必要的引用计数管理(但没有克隆)。
    【解决方案7】:

    如果您预先分配字符串,您可以获得更好的性能。

    function cwLeftPadMine
    {$IFDEF VER210}  //delphi 2010
    (aString: ansistring; aCharCount: integer; aChar: ansichar): ansistring;
    {$ELSE}
    (aString: string; aCharCount: integer; aChar: char): string;
    {$ENDIF}
    var
      i,n,padCount: integer;
    begin
      padCount := aCharCount - Length(aString);
    
      if padCount > 0 then begin
        //go ahead and set Result to what it's final length will be
        SetLength(Result,aCharCount);
        //pre-fill with our pad character
        FillChar(Result[1],aCharCount,aChar);
    
        //begin after the padding should stop, and restore the original to the end
        n := 1;
        for i := padCount+1 to aCharCount do begin
          Result[i] := aString[n];
        end;
      end
      else begin
        Result := aString;
      end;
    end;
    

    这是一个用于比较的模板:

    procedure TForm1.btnPadTestClick(Sender: TObject);
    const
      c_EvalCount = 5000;  //how many times will we run the test?
      c_PadHowMany = 1000;  //how many characters will we pad
      c_PadChar = 'x';  //what is our pad character?
    var
      startTime, endTime, freq: Int64;
      i: integer;
      secondsTaken: double;
      padIt: string;
    begin
      //store the input locally
      padIt := edtPadInput.Text;
    
      //display the results on the screen for reference
      //(but we aren't testing performance, yet)
      edtPadOutput.Text := cwLeftPad(padIt,c_PadHowMany,c_PadChar);
    
      //get the frequency interval of the OS timer    
      QueryPerformanceFrequency(freq);
    
      //get the time before our test begins
      QueryPerformanceCounter(startTime);
    
      //repeat the test as many times as we like
      for i := 0 to c_EvalCount - 1 do begin
        cwLeftPad(padIt,c_PadHowMany,c_PadChar);
      end;
    
      //get the time after the tests are done
      QueryPerformanceCounter(endTime);
    
      //translate internal time to # of seconds and display evals / second
      secondsTaken := (endTime - startTime) / freq;
      if secondsTaken > 0 then begin
        ShowMessage('Eval/sec = ' + FormatFloat('#,###,###,###,##0',
          (c_EvalCount/secondsTaken)));
      end
      else begin
        ShowMessage('No time has passed');
      end;
    end;
    

    使用该基准模板,我得到以下结果:

    The original: 5,000 / second
    Your first revision: 2.4 million / second
    My version: 3.9 million / second
    Rob Kennedy's version: 3.9 million / second
    

    【讨论】:

    • 是的,我现在正在做类似的事情。非常类似于 Rob 的回答(当我看到你的回答时我已经接受了)
    • @JosephStyons 与哪个版本相比戏剧性地?查看我的基准测试。
    • @Wodzu,与他原来的帖子相比,简直是天壤之别。正如您在示例中所做的那样预先缓存结果无疑会更快.. 不过,正如您所说,“值得吗”。
    【解决方案8】:

    这是我的解决方案。我使用 StringOfChar 而不是 FillChar 因为它可以处理 unicode 字符串/字符:

    function PadLeft(const Str: string; Ch: Char; Count: Integer): string;
    begin
      if Length(Str) < Count then
      begin
        Result := StringOfChar(Ch, Count);
        Move(Str[1], Result[Count - Length(Str) + 1], Length(Str) * SizeOf(Char));
      end
      else Result := Str;
    end;
    
    function PadRight(const Str: string; Ch: Char; Count: Integer): string;
    begin
      if Length(Str) < Count then
      begin
        Result := StringOfChar(Ch, Count);
        Move(Str[1], Result[1], Length(Str) * SizeOf(Char));
      end
      else Result := Str;
    end;
    

    【讨论】:

      猜你喜欢
      • 2015-11-27
      • 2011-03-01
      • 2021-03-02
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-09-21
      • 1970-01-01
      相关资源
      最近更新 更多