3

我是那些在没有真正理解甚至没有考虑基础知识的情况下就使用 Delphi 的所谓开发人员之一。在这种情况下,我说的是字符串

虽然我确实了解预分配内存如何导致显着的速度提升。我不明白如何在简单的真实案例中使用它(对于 TStringBuilder 更是如此)。

例如,假设我有这样的代码递归搜索文件夹并将结果添加到哈希列表:

var
   FilesList : TDictionary<String, Byte>;  // Byte = (file = 0, folder = 1)

// ------------------------------------------------------------------------------ //
procedure AddFolder(const AFolderName : String);
var
   FileName : String;
   AHandle  : THandle;
   FindData : TWin32FindData;
begin
     AHandle := FindFirstFile(PChar(AFolderName + '*'), FindData);
     if (AHandle = INVALID_HANDLE_VALUE) then
        Exit;

     repeat
           if (FindData.dwFileAttributes And FILE_ATTRIBUTE_DIRECTORY = 0) then
           begin
                { Add a file. }
                FileName := FindData.cFileName;
                FilesList.Add(AFolderName + FileName, 0);
           end
           else if ((FindData.cFileName[0] <> '.') OR Not ((FindData.cFileName[1] = #0) OR (FindData.cFileName[1] = '.') And (FindData.cFileName[2] = #0))) then
           begin
                FileName := AFolderName + FindData.cFileName + '\';
                FilesList.Add(FileName, 1);
                AddFolder(FileName);
           end;
     until Not FindNextFile(AHandle, FindData);

     Windows.FindClose(AHandle);
end;

我不确定这是否是一个很好的例子,但在这种情况下,我不清楚为变量预分配内存如何FileName有助于提高执行速度,尤其是我对它的长度一无所知。假设这是可能的,怎么办?

还是仅在连接/构建字符串时才使用预分配技术?


关于我的问题的注释:

  1. 问题主要针对XE2,但请随意参考其他 delphi 版本,因为我确信其他开发人员将从分享智慧中受益(也就是说,假设 mods 不会将其删除为健谈或主观)

  2. 我对需要通过优化字符串内存预分配在非常大的循环/大量数据中进行微优化的简单日常案例更感兴趣。

4

3 回答 3

9

直接的字符串连接(例如)可能会很慢,因为字符串的内存是为每个附加的片段重新分配的。有时新大小实际上可以就地容纳,但有时必须将数据复制到新位置,释放旧缓冲区,等等。这需要时间。

不过,一般来说,除非您已使用性能分析器或明确的时序声明确认您确实存在性能问题,否则您应该不会担心这一点。

于 2013-01-08T01:49:08.023 回答
4

当然,您在不了解字符串的真正工作原理的情况下就逃脱了:Delphi 在这方面非常出色,它的字符串操作非常有效,它的内存管理器对于小内存块也非常有效。你可以用 Delphi 做很多事情,并且对字符串操作没有问题。

您应该注意某些类别的问题,特别是如果您正在查看的例程将被重用(库代码)。

例如,这应该总是引发一个标志:

Result := '';
for i:=1 to N do
  Result := Result + Something; // <- Recursively builds the string, one-char-at-a-time

如果不经常使用或在时间不重要的情况下使用,即使这也可能与 Delphi 一起使用。尽管如此,应该优化这种代码,以便预先分配字符串的整个(可能)长度,然后在结束时修剪:

SetLength(Result, Whatever);
for i:=1 to N do
  Result[i] := SomeChar;
SetLength(Result, INowKnowTheLength);

现在举一个TStringBuilder闪耀的例子。如果你有这样的事情:

var Msg: string;
begin
  Msg := 'Mr ' + MrName + #13#10;
  if SomeCondition then
    Msg := Msg + 'We were unable to reach you'
  else
    Msg := Msg + 'We need to let you know';
  Msg := Msg + #13#10 
end;

即:构建一个复杂(并且可能很大)消息的代码,然后您可以使用以下方法轻松优化它TStringBuilder

var Msg: TStringBuilder;
begin
  Msg := TStringBuilder.Create;
  try
    Msg.Append('Mr ');
    Msg.Append(MrName);
    Msg.Append(#13#10);
    if SomeCondition then
      Msg.Append('We were unable to reach you')
    else
      Msg.Append('We need to let you know');
    Msg.Append(#13#10);
    ShowMessage(Msg.ToString); // <- Gets the whole string
  finally Msg.Free;
  end;
end;

无论如何,始终在易于编写、易于维护和性能的真正好处之间取得平衡。不要超出您编写的代码的自然限制:优化字符串生成例程以使其比 HDD 可以写入的速度更快是浪费精力。优化一些 GUI 代码以在 1 毫秒(而不是 20 毫秒)内生成消息也是浪费精力 - 用户永远不会知道您的代码快 20 倍,它会是即时的。

于 2013-01-08T12:10:47.407 回答
3

基本上,您正在谈论的是这些串联:

AFolderName + '*'
AFolderName + FindData.cFileName
AFolderName + FindData.cFileName + '\'

第一个执行一次,循环执行第二个和第三个。

System.pas 中的这些方法在内部用于 3 行:

procedure _UStrCat3(var Dest: UnicodeString; const Source1, Source2: UnicodeString);
procedure _UStrCat3(var Dest: UnicodeString; const Source1, Source2: UnicodeString);
procedure _UStrCatN(var Dest: UnicodeString; ArgCnt: Integer; const Strs: UnicodeString); varargs;

由于 3 个值不同,因此您无法仅使用一个表达式对其进行优化。

所有函数都会预先计算最终长度,并在需要时进行适当的分配工作。

在循环内部,您可能会尝试自己进行预分配AFolderName + FindData.cFileName + '\',然后取出该AFolderName + FindData.cFileName部分,但是您需要为then案例分配 2 次。

所以我认为你的代码不能得到进一步优化(即你不能让它更好地执行一个数量级)。

于 2013-01-08T10:29:51.680 回答