When and Why Should I Use TStringBuilder?
Basically, I use these idioms for building strings. The most important differences are:
- TStringBuilder.Create and Append pattern which adds new characters to the TStringBuilder instance.
- TStringList.Create and Add pattern which adds new lines the to the Text of the TStringList instance.
- The Format function to assemble strings based on format patterns.
- Simple concatenation of string types for expressions with 3 or fewer values.
For complex build patterns, the first make my code a lot cleaner, the second only if I add lines and often includes many of Format
calls.
The third makes my code cleaner when format patterns are important.
I use the last one only when the expression is very simple.
A few more differences between the first two idioms:
TStringBuilder
has many overloads forAppend
, and also has AppendLine (with only two overloads) if you want to add lines likeTStringList.Add
canTStringBuilder
reallocates the underlying buffer with an over capacity scheme, which means that with large buffers and frequent appends, it can be a lot faster thanTStringList
- To get the
TStringBuilder
content, you have to call the ToString method which can slow things down.
So: speed is not the most important matter to choose your string appending idiom. Readable code is.
To the best of my knowledge TStringBuilder was introduced just for some parity with .NET and Java, it seems to be more of a tick the box type feature than any major advance.
Consensus seems to be that TStringBuilder is faster in some operations but slower in others.
Your program sounds like an interesting one to do a before/after TStringBuilder comparison with but I wouldn't do it other than as an academic exercise.
I tried to improve a old routine that was parsing a text file (1.5GB). The routine was pretty dumb and it was building a string like this: Result:= Result+ buff[i];
So, I though that TStringBuilder will add significant speed improvements. It turned out that the 'dumb' code was actually 114% faster than the 'improved' version with TStringBuilder.
So, building a string from characters is NOT a place where you can obtain speed improvement with TStringBuilder.
My StringBuilder (below) is 184.82 times (yes 184!!!!!!) faster than the classic s:= s+ chr. (Experiment on a 4MB string)
Classic s:= s + c
Time: 8502 ms
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;
Prebuffered
Time:
BuffSize= 10000; // 10k buffer = 406ms
BuffSize= 100000; // 100k buffer = 140ms
BuffSize= 1000000; // 1M buffer = 46ms
Code:
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;
Prebuffered, as class
Time:
BuffSize= 10000; // 10k buffer = 437ms
BuffSize= 100000; // 100k buffer = 187ms
BuffSize= 1000000; // 1M buffer = 78ms
Code:
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;
And this is the class:
{ 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;
Conclusion: Stop using s:= s + c if you have large (over 10K) strings. It might be true even if you have small strings but you do it often (for example, you have a function that is doing some string processing on a small string, but you call it often).
_
PS: You may also want to see this: https://www.delphitools.info/2013/10/30/efficient-string-building-in-delphi/2/
TStringBuilder was introduced solely to provide a source code compatible mechanism for applications to perform string handling in Delphi and Delphi.NET. You sacrifice some speed in Delphi for some potentially significant benefits in Delphi.NET
The StringBuilder concept in .NET addresses performance issues with the string implementation on that platform, issues that the Delphi (native code) platform simply does not have.
If you are not writing code that needs to be compiled for both native code and Delphi.NET then there is simply no reason to use TStringBuilder.