【问题标题】:Delphi: Fast(er) widestring concatenationDelphi:快速(更)宽字符串连接
【发布时间】:2011-03-01 17:35:44
【问题描述】:

我有一个函数,他的工作是将 ADO Recordset 转换为 html:

class function RecordsetToHtml(const rs: _Recordset): WideString;

而且函数的核心涉及很多宽字符串连接:

   while not rs.EOF do
   begin
      Result := Result+CRLF+
         '<TR>';

      for i := 0 to rs.Fields.Count-1 do
         Result := Result+'<TD>'+VarAsWideString(rs.Fields[i].Value)+'</TD>';

      Result := Result+'</TR>';
      rs.MoveNext;
    end;

如果有几千个结果,任何用户都会觉得该函数运行时间太长。 Delphi Sampling Profiler 表明 99.3% 的时间用于宽字符串连接(@WStrCatN@WstrCat)。

任何人都可以想出一种改进宽字符串连接的方法吗?我认为 Delphi 5 没有任何类型的字符串生成器。而且Format 不支持Unicode。


为了确保没有人试图偷懒:假装你正在实现接口:

IRecordsetToHtml = interface(IUnknown)
    function RecordsetToHtml(const rs: _Recordset): WideString;
end;

更新一

我曾想过使用IXMLDOMDocument 将HTML 构建为xml。但后来我意识到最终的 HTML 将是 xhtml 而不是 html - 一个微妙但重要的区别。

更新二

微软知识库文章:How To Improve String Concatenation Performance

【问题讨论】:

    标签: delphi performance delphi-5 widestring


    【解决方案1】:

    WideString 本质上很慢,因为它们是为实现 COM 兼容性而实现的,并通过 COM 调用。如果您查看代码,它将继续重新分配字符串并调用 SysAllocStringLen() 和 C,它们是来自 oleaut32.dll 的 API。它不使用 Delphi 内存管理器,但 AFAIK 它使用 COM 内存管理器。 因为大多数 HTML 页面不使用 UTF-16,所以使用原生 Delphi 字符串类型和字符串列表可能会得到更好的结果,尽管你应该小心从 UTF 和实际代码页转换,并且转换也会降低性能. 此外,您正在使用 VarAsString() 函数,该函数可能将变体转换为 AnsiString,然后再转换为 WideString。检查您的 Delphi 版本是否有 VarAsWideString() 或类似的函数来避免它,或者如果您可以确定您的变体永远不会为 NULL,请依赖 Delphi 自动转换。

    【讨论】:

    • SysAllocString 本身并不比从 Delphi 的内部堆分配内存慢。
    • 通过 COM API 恕我直言,可能比从内存管理器池中分配内存块的过程要慢一些。不过,我没有基准。
    • @IanBoyd SysAllocStringSysFreeString ARE 比 Delphi 的 FastMM4 慢得多。只需替换 API 调用,当分配大量 WideString 实例时,您将获得 50 倍的加速 - 请参阅 blog.synopse.info/post/2014/09/12/…
    【解决方案2】:

    是的,你的算法显然是 O(n^2)。

    不要返回string,而是尝试返回TStringList,并将循环替换为

       while not rs.EOF do
       begin
          Result.Add('<TR>');
    
          for i := 0 to rs.Fields.Count-1 do
             Result.Add( '<TD>'+VarAsString(rs.Fields[i].Value)+'</TD>' );
    
          Result := Result.Add('</TR>');
          rs.MoveNext;
        end;
    

    然后您可以使用TStringList.SaveToFile 保存您的Result

    【讨论】:

    • Stringlist 不支持 WideStrings
    【解决方案3】:

    我现在无法花时间为您提供确切的代码。

    但我认为你能做的最快的事情是:

    1. 遍历所有字符串并计算它们的总长度,同时添加您需要的额外表格标签。

    2. 使用 SetString 分配一个适当长度的字符串。

    3. 再次循环遍历所有字符串并使用the "Move" procedure 将字符串复制到最终字符串中的适当位置。

    关键是由于不断分配和释放内存,许多连接到字符串的时间越来越长。一次分配将是您最大的节省时间。

    【讨论】:

    • 理想的解决方案,字符串生成器,使用了这些概念。
    【解决方案4】:

    我找到了最好的解决方案。 Delphi 的开源HtmlParser,有一个助手TStringBuilder 类。它在内部用于构建他所谓的DomStrings,实际上是WideString的别名:

    TDomString = WideString;
    

    稍微摆弄一下他的班级:

    TStringBuilder = class
    public
       constructor Create(ACapacity: Integer);
       function EndWithWhiteSpace: Boolean;
       function TailMatch(const Tail: WideString): Boolean;
       function ToString: WideString;
       procedure AppendText(const TextStr: WideString);
       procedure Append(const value: WideString);
       procedure AppendLine(const value: WideString);
       property Length: Integer read FLength;
    end;
    

    例程的胆量变成:

    while not rs.EOF do
    begin
       sb.Append('<TR>');
    
       for i := 0 to rs.Fields.Count-1 do
          sb.Append('<TD>'+VarAsWideString(rs.Fields[i].Value));
    
       sb.AppendLine('</TR>');
    
       rs.MoveNext;
    end;
    

    然后代码感觉运行得无限快。分析显示很大改进; WideString 操作和长度计数变得可以忽略不计。取而代之的是 FastMM 自己的内部运营。

    备注

    1. 很好地发现了错误地将所有字符串强制转换为当前代码页(VarAsString 而不是VarAsWideString
    2. 一些 HTML 结束标签是可选的;省略了逻辑上没有意义的部分。

    【讨论】:

      【解决方案5】:

      Widestring 不被引用计数,任何修改都意味着字符串操作。 如果您的内容不是 unicode 编码的,您可以在内部使用本机字符串(引用计数)连接字符串,然后将其转换为 Widestring。示例如下:

      var
        NativeString: string;
      begin
         // ...
         NativeString := '';
      
         while not rs.EOF do
         begin
           NativeString := NativeString + CRLF + '<TR>';
      
           for i := 0 to rs.Fields.Count-1 do
             NativeString := NativeString + '<TD>'+VarAsString(rs.Fields[i].Value) + '</TD>';
      
           NativeString := NativeString + '</TR>';
           rs.MoveNext;
         end;
      
         Result := WideString(NativeString);
      

      我还看到了另一种方法:将 Unicode 编码为 UTF8String(作为引用计数),将它们连接起来,最后将 UTF8String 转换为 Widestring。但我不确定,是否可以直接连接两个 UTF8String。还应考虑编码时间。

      无论如何,尽管 Widestring 连接比原生字符串操作慢得多。但它仍然是 IMO 可以接受的。应该避免对这类事情进行过多的调整。 认真考虑性能,然后您应该将您的 Delphi 升级到至少 2009 年。购买工具的成本比在老德尔福。

      【讨论】:

      • 我没有几万美元来支付升级 Delphi 的所有费用。 (另外,我无法忍受 IDE,或者它的性能。我们已经过渡到一个新的 IDE,它是微软免费的基于 C# 的 IDE)
      • 而“调整过多”或“早期优化是万恶之源”的概念在这里并不适用。我当时无法显示屏幕截图,但分析指出了最终用户抱怨的确切瓶颈。如果不是这么慢,我不会在意的。这不是什么幻想。
      猜你喜欢
      • 1970-01-01
      • 2010-12-13
      • 2020-12-09
      • 2018-10-04
      • 2020-12-18
      • 2013-01-03
      • 2011-08-30
      • 2011-04-24
      • 2016-09-18
      相关资源
      最近更新 更多