什么时候和为什么要使用TStringBuilder?

15

我一年前将我的程序从Delphi 4转换为Delphi 2009,主要是为了实现Unicode的跃升,同时也为了获得所有这些年来Delphi改进的好处。

我的代码当然都是旧的遗留代码。它使用短字符串,现在方便地全部变成了长Unicode字符串,并且我已经将所有旧的ANSI函数更改为新的等效函数。

但是,在Delphi 2009中,他们引入了TStringBuilder类,可能是以.NET的StringBuilder类为模型。

我的程序做了很多字符串处理和操作,并可以一次性将数百兆字节的大字符串加载到内存中进行处理。

我不太了解Delphi对TStringBuilder类的实现,但我听说它的某些操作比使用默认字符串操作更快。

我的问题是,是否值得花费时间将我的标准字符串转换为使用TStringBuilder类。我会从中获得什么,会失去什么?


感谢您的答案并引导我得出结论,即除非需要.NET兼容性,否则不必费事。

在他在Delphi 2009 String Performance上的博客中,Jolyon Smith表示:

但是,看起来TStringBuilder主要是.NET兼容性的固定装置,而不是为Win32应用程序的开发人员提供任何实际好处,可能会有开发人员希望或需要单一源Win32/.NET代码库,在这种情况下,字符串处理性能不是一个问题。


你是否注意到当你从D4的shortstrings转换成D2009的unicode strings时,字符串性能有所变化呢? - LachlanG
2
我没有直接对字符串进行计时,但我的升级导致代码性能提高了25% - 这可能是由于FastMM和其他内置于新版本中的优化所致。外部ANSI文件必须编码为Unicode,这需要双倍的空间,并且对于非常大的文件,这会给程序增加重大负担,从而扭转了小文件的性能改进。将数据块分成非常大的缓冲区可以减轻负担。总体而言,我觉得我的程序可能与以前一样快,但具有Unicode的巨大好处。 - lkessler
2
@lkessler:你还在重复“必须将外部ANSI文件编码为Unicode”的错误信息,这真是令人悲哀。如果你将它们转换为UTF-8(这是一种有效的Unicode编码),你的文件大小就不会增加,而且你不会失去任何东西。相反,除非使用快速SSD,否则I/O下降可能比字符串重新编码的CPU周期增加更重要,从而为您提供良好的性能提升。 - mghie
Lachlan:还可以参考Jan Goyvaert的文章:“使用本机Win32字符串类型的速度优势”http://www.micro-isv.asia/2008/09/speed-benefits-of-using-the-native-win32-string-type/ - lkessler
Mghie:据我所知,UTF8 不是 Windows 的本地编码。每次处理 UTF8 字符串都需要进行转换。因此,空间和处理速度之间存在权衡。但既然你提到了这一点,当我回到输入处理时,我会尝试将其作为 UTF8 加载到内存中,并比较整体处理速度。如果 UTF8 在处理方面的开销不太大,那么我会保持这种方式。再次感谢你指出这一点,因为你可能帮助了我很多。 - lkessler
6个回答

13

据我所知,TStringBuilder似乎只是为了与.NET和Java保持一致,它似乎更像是一种“打勾”式的功能而不是任何重大进展。

共识似乎是,在某些操作中,TStringBuilder比其他方法更快,但在其他操作中则较慢。

你的程序听起来很有趣,可以用TStringBuilder进行前后速度比较,但我只会将其作为一种学术练习,而不是其他用途。


11

基本上,我使用这些成语来构建字符串。最重要的区别在于:

对于复杂的构建模式,第一个方法可以使我的代码更加清晰,第二个方法仅在我添加行并且经常包含许多Format调用时使用。

第三个方法可以使我的代码更加简洁,当格式模式很重要时使用。

只有当表达式非常简单时,我才使用最后一个方法。

第一个和第二个方法之间还有一些区别:

  • TStringBuilder 有许多 Append 的重载,并且还有 AppendLine(仅有两个重载)可用于添加类似于 TStringList.Add 的行。
  • TStringBuilder 使用超出容量的方案重新分配基础缓冲区,这意味着对于大缓冲区和频繁附加的情况,它可能比 TStringList 更快。
  • 要获取 TStringBuilder 的内容,必须调用 ToString 方法,这可能会减慢速度。

因此:选择字符串附加习惯并不是速度最重要的问题。 可读性更重要。


11
我尝试改进一份解析文本文件(1.5GB)的旧例程。这个例程相当愚笨,它类似这样构建字符串:s:= s+ buff[i];
于是,我想到TStringBuilder会带来显著的速度提升。但实际上,它比原来的方法慢了114%。
因此,我自己编写了一个StringBuilder,它的速度比经典的s:= s + chr (在4MB字符串上进行的实验)快184.82倍(是的,184!!!!!!),甚至比TStringBuilder还要快。 测试结果: 经典的 s:= s + c
时间:8502毫秒
procedure TfrmTester.btnClassicClick(Sender: TObject);
VAR
   s: string;
   FileBody: string;
   c: Cardinal;
   i: Integer;
begin
 FileBody:= ReadFile(File4MB);
 c:= GetTickCount;
 for i:= 1 to Length(FileBody) DO
  s:= s+ FileBody[i];
 Log.Lines.Add('Time: '+ IntToStr(GetTickCount-c) + 'ms');     // 8502 ms
end;

预缓冲

Time:  
     BuffSize= 10000;       // 10k  buffer = 406ms
     BuffSize= 100000;      // 100k buffer = 140ms
     BuffSize= 1000000;     // 1M   buffer = 46ms

代码:

procedure TfrmTester.btnBufferedClick(Sender: TObject);
VAR
   s: string;
   FileBody: string;
   c: Cardinal;
   CurBuffLen, marker, i: Integer;
begin
 FileBody:= ReadFile(File4MB);
 c:= GetTickCount;

 marker:= 1;
 CurBuffLen:= 0;
 for i:= 1 to Length(FileBody) DO
  begin
   if i > CurBuffLen then
    begin
     SetLength(s, CurBuffLen+ BuffSize);
     CurBuffLen:= Length(s)
    end;
   s[marker]:= FileBody[i];
   Inc(marker);
  end;

 SetLength(s, marker-1); { Cut down the prealocated buffer that we haven't used }  
 Log.Lines.Add('Time: '+ IntToStr(GetTickCount-c) + 'ms');
 if s <> FileBody
 then Log.Lines.Add('FAILED!');
end;

预缓存,作为类
Time:    
 BuffSize= 10000;       // 10k  buffer = 437ms       
 BuffSize= 100000;      // 100k buffer = 187ms        
 BuffSize= 1000000;     // 1M buffer = 78ms     

代码:

procedure TfrmTester.btnBuffClassClick(Sender: TObject);
VAR
   StringBuff: TCStringBuff;
   s: string;
   FileBody: string;
   c: Cardinal;
   i: Integer;
begin
 FileBody:= ReadFile(File4MB);
 c:= GetTickCount;

 StringBuff:= TCStringBuff.Create(BuffSize);
 TRY
   for i:= 1 to Length(FileBody) DO
    StringBuff.AddChar(filebody[i]);
   s:= StringBuff.GetResult;
 FINALLY
  FreeAndNil(StringBuff);
 END;

 Log.Lines.Add('Time: '+ IntToStr(GetTickCount-c) + 'ms');
 if s <> FileBody
 then Log.Lines.Add('FAILED!');
end;

这是该类:

{ TCStringBuff }

constructor TCStringBuff.Create(aBuffSize: Integer= 10000);
begin
 BuffSize:= aBuffSize;
 marker:= 1;
 CurBuffLen:= 0;
 inp:= 1;
end;

function TCStringBuff.GetResult: string;
begin
 SetLength(s, marker-1);                    { Cut down the prealocated buffer that we haven't used }
 Result:= s;
 s:= '';         { Free memory }
end;

procedure TCStringBuff.AddChar(Ch: Char);
begin
 if inp > CurBuffLen then
  begin
   SetLength(s, CurBuffLen+ BuffSize);
   CurBuffLen:= Length(s)
  end;

 s[marker]:= Ch;
 Inc(marker);
 Inc(inp);
end;
结论:

如果您有大型字符串(超过10K),请停止使用s:= s + c。 即使您有小字符串,但经常这样做(例如,您有一个在小字符串上进行某些字符串处理的函数,但经常调用它),也可能是正确的。

_

PS:您还可以查看此链接:https://www.delphitools.info/2013/10/30/efficient-string-building-in-delphi/2/


2
感谢您添加这个晚回答。您提供的链接是一篇非常优秀的文章。 - lkessler

8

TStringBuilder是为了提供一种源代码兼容机制,让应用程序在DelphiDelphi.NET中执行字符串处理而引入的。在Delphi中,你需要牺牲一些速度,但在Delphi.NET中可能会获得一些显著的好处。

.NET中的StringBuilder概念解决了该平台上字符串实现的性能问题,而这些问题在Delphi(本地代码)平台上根本不存在。

如果您不编写需要编译成本地代码和Delphi.NET的代码,则根本没有使用TStringBuilder的理由。


3
它不仅仅是为了源代码兼容性而引入的,这只是其中的一部分。另一个强烈的原因是它是一个强大的类别,并且由于一些人喜欢它的流畅编码模式。最重要的是,如果你想用它就用,不想用就算了。 - Nick Hodges
1
真的好奇:“强大”在哪里?为什么?至于流利?嗯,就像你说的,如果你想用,就用;如果不想用(或者在调试时想保持理智),就不用。 - Deltics
@Deltics - 或许TStringBuilder确实是为了与.NET兼容性而引入的,但我猜这大约占了95%的原因;-) 如果Delphi.NET的要求从未存在,那么TStringBuilder被引入的几率有多大? - IanH
5
Mike Lischke的VirtualTrees比Embarcadero早几年就引入了TStringBuilder。它被创建出来加速Unicode字符串的操作。唯一的支持来自于Windows的BSTRs,Delphi将其作为WideString暴露出来。Windows没有提供任何方法来realloc一个BSTR(BSTR不是引用计数的; SysReallocString会创建第二个字符串并进行复制)。在这种情况下,Delphi的TStringBuilder是必需品。此后,如果您要添加大量内容,则设置字符串的Capacity是有用的。此外,它具有良好的语法。 - Ian Boyd

7

根据Marco Cantu的说法,使用TStringBuilder并非为了加速,而是可以得到更干净、更兼容.Net的代码。 这里(还有一些修正在这里)另外一个TStringBuilder的速度测试显示,并不比其他方式更快。


5
谢谢提供链接,对我有所帮助。但我不能同意你关于StringBuilder可以使代码更简洁的观点。我认为使用 s := s + s2; 要比 SB.Append(s2); 更好。 - lkessler
从Marco Cantu的文章中,我相信他的意思是在添加各种数据类型时。当然,并没有太大的区别。 - stg
1
这是Cantu的字符串连接测试:"for i := 1 to 15 do s := s + 'xxx';" 这不算什么测试。for循环应该更大才行。我敢打赌,在那种情况下TStringBuilder会胜出。不幸的是,我无法测试它,因为我没有D2009。一般来说,在大多数语言中,这种模式都很慢,因为字符串必须不断重新分配和复制。 - dan-gph
但我可能错了。我只是用大型循环进行了快速测试,它并没有像我预期的那样停滞不前。我猜Delphi必须在原地修改字符串并每次分配额外空间。对于具有不可变字符串的语言,循环会变得越来越慢。 - dan-gph

6
基本上只是一个跟风的功能,就像LachlanG说的那样。在.NET中需要它是因为CLR字符串是不可变的,但Delphi没有这个问题,因此并不真正需要一个字符串构建器作为解决方法。

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