Friday, April 29, 2016

Is (Embarcadero) Delphi's code optimized for speed? No? Here is how to optimize string speed

The short answer is a big NO!

No the long answer:
Today I needed a function that will wrap a string (a contiguous block of characters with no spaces) after 80 characters. Not only that I have found SysUtils.WrapText unsuitable (it can only wrap text IF the text contains spaces) but it is also terrible slow.

So I build my own function:

function WrapString(CONST s: string; RowLength: integer): string;
VAR i, Row: Integer;
Begin
 Row:= 0;
 Result:= '';
 for i:= 1 TO Length(s) DO
  begin
   inc(Row);
   Result:= Result+ s[i];
   if Row >= RowLength then
    begin
     Result:= Result+ CRLF; 

     Row:= 0;
    end;
  end;
End;


Works nice but is is also slow. If you look into the code the problem is Result:= Result+ CRLF . It involves too many memory allocations.

Solution. The solution is to pre-allocate space for the result.
For this I created a new class TCStringBuilder:


TYPE
 TCStringBuilder = class(TObject)
  private
   s: string;
   CurBuffLen, BuffPos: Integer;
  public
   BuffSize: Integer;
   constructor Create(aBuffSize: Integer= 10*Kb);
   procedure AddChar(Ch: Char);
   procedure AddEnter;

   function  AsText: string;
   procedure Clear;
 end;



IMPLEMENTATION

constructor TCStringBuilder.Create(aBuffSize: Integer= 10*Kb);
begin
 BuffSize:= aBuffSize;
 Clear;
end;


procedure TCStringBuilder.Clear;
begin
 BuffPos:= 1;
 CurBuffLen:= 0;
 s:= '';
end;


function TCStringBuilder.AsText: string;
begin
 SetLength(s, BuffPos-1);                    { Cut down the prealocated buffer that we haven't used }
 Result:= s;
end;


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

 s[BuffPos]:= Ch;
 Inc(BuffPos);
end;


procedure TCStringBuilder.AddEnter;
begin
 if BuffPos+1 > CurBuffLen then    { +1 because we enter two characters into the string instead of 1 }
  begin
   SetLength(s, CurBuffLen+ BuffSize);
   CurBuffLen:= Length(s)
  end;

 s[BuffPos  ]:= CR;
 s[BuffPos+1]:= LF;
 Inc(BuffPos, 2);
end;



Speed test:
  • 500x loop
  • test file: TesterForm.pas 2.7K
  • wrap after 20 chars
Speed test results:
  •   484ms  SysUtils.WrapText - unbuffered
  •   5788ms WrapString        - unbuffered (Result:= Result+ s[i])
  •   31ms   WrapString        - buffered (using cStrBuilder)

I used a buffer of 10K. but the ideal buffer size would be the size of the input text plus 3%.

Please let me know if you can further improve this. Enjoy.

____

Further reading:
https://www.delphitools.info/2013/10/30/efficient-string-building-in-delphi/2/

No comments:

Post a Comment