Jump to content
Mike Torrettinni

Performance of MOVE vs + for concatenating mixed type values

Recommended Posts

I have arrays that I export as csv file format, all mixed field types. I was trying to compare simple concatenation + vs MOVE to create lines to export, but MOVE is 30% slower than simple + in example below:

 

Concatenating integer, boolean and char value require more operations (calculate length) than simple concatenating pure strings Is that the reason MOVE is just not as performant as simple + ?

Or perhaps I use MOVE wrong in case of mixed types of fields?

 

In example I have PrepareLineForExport that uses + for concatenation, and PrepareLineForExport_MOVE that uses MOVE, I was expecting MOVE to be faster:

 

uses System.Diagnostics;

type
  TData = record
    ID1: integer;
    S1: string;
    B1: boolean;
    S2: string;
    C1: char;
    S3: string;
  end;
var
  xData: TArray<TData>;
const
  cMaxLines = 10000000;
  cSeparator = ';';


procedure FillTestData;
var i: Integer;
begin
  xData := nil;
  SetLength(xData, cMaxLines);
  for i := Low(xData) to High(xData) do
  begin
    xData[i].ID1 := i;
    xData[i].S1 := 'Testing array data export';
    xData[i].B1 := True;
    xData[i].S2 := 'String 2';
    xData[i].C1 := 'A';
    xData[i].S3 := 'String 3';
  end;
end;

function PrepareLineForExport(const aDataIn: TData): string;
begin
  Result :=
    aDataIn.ID1.ToString  + cSeparator +
    aDataIn.S1            + cSeparator +
    BoolToStr(aDataIn.B1) + cSeparator +
    aDataIn.S2            + cSeparator +
    aDataIn.C1            + cSeparator +
    aDataIn.S3;
end;

function PrepareLineForExport_MOVE(const aDataIn: TData): string;
var vPos, vLen: integer;
    vStr: string;
begin
  // Len = 5 x Separator + Len of Data
  SetLength(Result, 5 + Length(aDataIn.ID1.ToString) + Length(aDataIn.S1) + Length(BoolToStr(aDataIn.B1)) + Length(aDataIn.S2) + 1 + Length(aDataIn.S3));

  vPos := 1;

  // ID1
  vStr := aDataIn.ID1.ToString;
  vLen := Length(vStr);
  Move(vStr[1], Result[vPos], vLen * SizeOf(Char));
  Inc(vPos, vLen);

  // Separator
  Result[vPos] := cSeparator; Inc(vPos, 1);

  // S1
  vLen := Length(aDataIn.S1);
  Move(aDataIn.S1[1], Result[vPos], vLen * SizeOf(Char));
  Inc(vPos, vLen);

  // Separator
  Result[vPos] := cSeparator; Inc(vPos, 1);

  // B1
  vStr := BoolToStr(aDataIn.B1);
  vLen := Length(vStr);
  Move(vStr[1], Result[vPos], vLen * SizeOf(Char));
  Inc(vPos, vLen);

  // Separator
  Result[vPos] := cSeparator; Inc(vPos, 1);

  // S2
  vLen := Length(aDataIn.S2);
  Move(aDataIn.S2[1], Result[vPos], vLen * SizeOf(Char));
  Inc(vPos, vLen);

  // Separator
  Result[vPos] := cSeparator; Inc(vPos, 1);

  // C1
  Result[vPos] := aDataIn.C1; Inc(vPos, 1);

  // Separator
  Result[vPos] := cSeparator; Inc(vPos, 1);

  // S3
  vLen := Length(aDataIn.S3);
  Move(aDataIn.S3[1], Result[vPos], vLen * SizeOf(Char));
end;

// Make sure both methods create same data for export!
procedure TestData;
var
  i: Integer;
begin
  for i := Low(xData) to High(xData) do
  begin
    if PrepareLineForExport(xData[i]) <> PrepareLineForExport_MOVE(xData[i]) then
      raise Exception.Create('Data build incorrect!');
  end;
end;

procedure TForm2.Button1Click(Sender: TObject);
var i: Integer;
  vLine, vLine2: string;
  vSW: TStopWatch;
begin
  FillTestData;
  TestData;

  vSW := TStopwatch.StartNew;
  for i := Low(xData) to High(xData) do
  begin
    vLine := '';
    vLine := PrepareLineForExport(xData[i]);
  end;
  memo1.Lines.Add('PrepareLineForExport: ' + vSW.ElapsedMilliseconds.ToString);

  vSW := TStopwatch.StartNew;
  for i := Low(xData) to High(xData) do
  begin
    vLine := '';
    vLine := PrepareLineForExport_MOVE(xData[i]);
  end;
  memo1.Lines.Add('PrepareLineForExport_MOVE: ' + vSW.ElapsedMilliseconds.ToString);
end;


 

 

Edited by Mike Torrettinni

Share this post


Link to post

In the SetLength call you're calling Integer.ToString and BoolToStr in order to determine the resultant string lengths, and lower down you're calling those again to get the actual strings. I reckon that's where the 30% is going.

 

Even if you fix that I doubt you'll see more than a marginal performance improvement going from PrepareLineForExport and PrepareLineForExport_MOVE. String concatenation is decently implemented in the RTL. (The 32-bit implementation is even in assembly language.)

 

  • Thanks 1

Share this post


Link to post

Note that you don't store the content, you re-assign each new line.

So you are testing something non reallistic, which is worthless trying to optimize it.

 

What is slow is not data moving, but memory allocation.

One performance problem is the temporary allocation of strings, if you call Integer.ToString.

For our mORMot TTextWriter, we don't use any temporary allocation, and even have pre-computed values for the smallest integers.

 

Note that Delphi TStringBuilder will actually be slower on Win32 than naive concatenation.

It also allocate a temporary string for integer appending... 😞

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

 

I would stick with naive string concatenation, and I guess it will be fast enough in practice.

It would be premature optimization otherwise.

 

 

  • Thanks 1

Share this post


Link to post
Guest

I tried to imagine the difference with StringBuilder against other methods, so here an expanded version of that test

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils,
  System.Diagnostics;

type
  TData = record
    ID1: integer;
    S1: string;
    B1: boolean;
    S2: string;
    C1: char;
    S3: string;
  end;

var
  xData: TArray<TData>;

const
  cMaxLines = 10000000;
  cSeparator = ';';

procedure FillTestData;
var
  i: Integer;
begin
  xData := nil;
  SetLength(xData, cMaxLines);
  for i := Low(xData) to High(xData) do
  begin
    xData[i].ID1 := i;
    xData[i].S1 := 'Testing array data export';
    xData[i].B1 := True;
    xData[i].S2 := 'String 2';
    xData[i].C1 := 'A';
    xData[i].S3 := 'String 3';
  end;
end;

function PrepareLineForExport(const aDataIn: TData): string;
begin
  Result := aDataIn.ID1.ToString + cSeparator + aDataIn.S1 + cSeparator + BoolToStr(aDataIn.B1) + cSeparator + aDataIn.S2 + cSeparator + aDataIn.C1 + cSeparator + aDataIn.S3;
end;

function PrepareLineForExport_MOVE(const aDataIn: TData): string;
var
  vPos, vLen: integer;
  vStr: string;
begin
  // Len = 5 x Separator + Len of Data
  SetLength(Result, 5 + Length(aDataIn.ID1.ToString) + Length(aDataIn.S1) + Length(BoolToStr(aDataIn.B1)) + Length(aDataIn.S2) + 1 + Length(aDataIn.S3));
  //SetLength(Result,255);
  vPos := 1;

  // ID1
  vStr := aDataIn.ID1.ToString;
  vLen := Length(vStr);
  Move(vStr[1], Result[vPos], vLen * SizeOf(Char));
  Inc(vPos, vLen);

  // Separator
  Result[vPos] := cSeparator;
  Inc(vPos, 1);

  // S1
  vLen := Length(aDataIn.S1);
  Move(aDataIn.S1[1], Result[vPos], vLen * SizeOf(Char));
  Inc(vPos, vLen);

  // Separator
  Result[vPos] := cSeparator;
  Inc(vPos, 1);

  // B1
  vStr := BoolToStr(aDataIn.B1);
  vLen := Length(vStr);
  Move(vStr[1], Result[vPos], vLen * SizeOf(Char));
  Inc(vPos, vLen);

  // Separator
  Result[vPos] := cSeparator;
  Inc(vPos, 1);

  // S2
  vLen := Length(aDataIn.S2);
  Move(aDataIn.S2[1], Result[vPos], vLen * SizeOf(Char));
  Inc(vPos, vLen);

  // Separator
  Result[vPos] := cSeparator;
  Inc(vPos, 1);

  // C1
  Result[vPos] := aDataIn.C1;
  Inc(vPos, 1);

  // Separator
  Result[vPos] := cSeparator;
  Inc(vPos, 1);

  // S3
  vLen := Length(aDataIn.S3);
  Move(aDataIn.S3[1], Result[vPos], vLen * SizeOf(Char));
end;

function PrepareLineForExport_Binary(const aDataIn: TData): string;
var
  Curr, Len: integer;
  S1Len, S2Len, S3Len: Integer;
begin
  S1Len := Length(aDataIn.S1) * SizeOf(Char);
  S2Len := Length(aDataIn.S2) * SizeOf(Char);
  S3Len := Length(aDataIn.S3) * SizeOf(Char);
  Len := {3strings with header } 3 * SizeOf(integer) + (S1Len + S2Len + S3Len) + { Id1}4 + {Boolean}     + 1 + {char}SizeOf(Char);
  SetLength(Result, Len);
  Curr := 1;
  PInteger(@Result[Curr])^ := aDataIn.ID1;
  Inc(Curr, 4);
  PInteger(@Result[Curr])^ := Length(aDataIn.S1);
  Inc(Curr, 4);
  Move(aDataIn.S1[1], Result[Curr], S1Len);
  Inc(Curr, S1Len);
  PByte(@Result[Curr])^ := Ord(aDataIn.B1);
  Inc(Curr, 1);
  PInteger(@Result[Curr])^ := Length(aDataIn.S2);
  Inc(Curr, 4);
  Move(aDataIn.S2[1], Result[Curr], S2Len);
  Inc(Curr, S2Len);
  PChar(@Result[Curr])^ := aDataIn.C1;
  Inc(Curr, SizeOf(Char));
  PInteger(@Result[Curr])^ := Length(aDataIn.S3);
  Inc(Curr, 4);
  Move(aDataIn.S3[1], Result[Curr], S3Len);
end;

function PrepareLineForExport_SB(const aDataIn: TData): string;
var
  StringBuilder: TStringBuilder;
begin
  StringBuilder := TStringBuilder.Create;
  try
    StringBuilder.Append(aDataIn.ID1.ToString).Append(cSeparator).Append(aDataIn.S1).Append(cSeparator);
    StringBuilder.Append(BoolToStr(aDataIn.B1)).Append(cSeparator).Append(aDataIn.S2).Append(cSeparator);
    StringBuilder.Append(aDataIn.S2).Append(cSeparator).Append(aDataIn.C1).Append(cSeparator);
    StringBuilder.Append(aDataIn.S3);
    Result := StringBuilder.ToString;
  finally
    StringBuilder.Free;
  end;
end;

function PrepareLineForExport_SB_Ex(const aDataIn: TData; var StringBuilder: TStringBuilder): string;
begin
  StringBuilder.Clear;
  StringBuilder.Append(aDataIn.ID1.ToString).Append(cSeparator).Append(aDataIn.S1).Append(cSeparator);
  StringBuilder.Append(BoolToStr(aDataIn.B1)).Append(cSeparator).Append(aDataIn.S2).Append(cSeparator);
  StringBuilder.Append(aDataIn.S2).Append(cSeparator).Append(aDataIn.C1).Append(cSeparator);
  StringBuilder.Append(aDataIn.S3);
  Result := StringBuilder.ToString;
end;

var
  i: Integer;
  vLine, vLine2: string;
  vSW: TStopWatch;
  SB: TStringBuilder;

begin
  FillTestData;
  //TestData;

  vSW := TStopwatch.StartNew;
  for i := Low(xData) to High(xData) do
  begin
    vLine := '';
    vLine := PrepareLineForExport(xData[i]);
  end;
  Writeln('PrepareLineForExport: ' + vSW.ElapsedMilliseconds.ToString);

  vSW := TStopwatch.StartNew;
  for i := Low(xData) to High(xData) do
  begin
    vLine := '';
    vLine := PrepareLineForExport_MOVE(xData[i]);
  end;
  Writeln('PrepareLineForExport_MOVE: ' + vSW.ElapsedMilliseconds.ToString);

  vSW := TStopwatch.StartNew;
  for i := Low(xData) to High(xData) do
  begin
    vLine := '';
    vLine := PrepareLineForExport_SB(xData[i]);
  end;
  Writeln('PrepareLineForExport_SB: ' + vSW.ElapsedMilliseconds.ToString);

  SB := TStringBuilder.Create;
  try
    vSW := TStopwatch.StartNew;
    for i := Low(xData) to High(xData) do
    begin
      vLine := '';
      vLine := PrepareLineForExport_SB_Ex(xData[i], SB);
    end;
    Writeln('PrepareLineForExport_SB_Ex: ' + vSW.ElapsedMilliseconds.ToString);
  finally
    SB.Free;
  end;

  vSW := TStopwatch.StartNew;
  for i := Low(xData) to High(xData) do
  begin
    vLine := '';
    vLine := PrepareLineForExport_Binary(xData[i]);
  end;
  Writeln('PrepareLineForExport_Binary: ' + vSW.ElapsedMilliseconds.ToString);

  Readln;
end.

and the result confirming Pierre and Arnaud 

Quote

PrepareLineForExport: 2163
PrepareLineForExport_MOVE: 3029
PrepareLineForExport_SB: 4761
PrepareLineForExport_SB_Ex: 3579
PrepareLineForExport_Binary: 714

 

Share this post


Link to post

I'm testing with David's Buffered streams:

 

PrepareLineForExport: 6184
PrepareLineForExport_MOVE: 6561

 

uses System.Diagnostics, BufferedStreams;

procedure TForm2.Button1Click(Sender: TObject);
var
  vFileStrm: TWriteCachedFileStream;
  vFileOut: TStreamWriter;
  i: Integer;
  vLine: string;
  vSW: TStopWatch;
begin
  FillTestData;
  TestData;

  vSW := TStopwatch.StartNew;
  vFileStrm := TWriteCachedFileStream.Create('c:\tmp\dataexport_'+FormatDateTime('hhnnsszzz',now())+'.txt');
  vFileOut := TStreamWriter.Create(vFileStrm, TEncoding.UTF8, 512);
  for i := Low(xData) to High(xData) do
  begin
    vLine := PrepareLineForExport(xData[i]);
    vFileOut.WriteLine(vLine);
  end;
  vFileOut.Free;
  vFileStrm.Free;
  memo1.Lines.Add('PrepareLineForExport: ' + vSW.ElapsedMilliseconds.ToString);

  vSW := TStopwatch.StartNew;
  vFileStrm := TWriteCachedFileStream.Create('c:\tmp\dataexport_'+FormatDateTime('hhnnsszzz',now())+'.txt');
  vFileOut := TStreamWriter.Create(vFileStrm, TEncoding.UTF8, 512);
  for i := Low(xData) to High(xData) do
  begin
    vLine := PrepareLineForExport_MOVE(xData[i]);
    vFileOut.WriteLine(vLine);
  end;
  vFileOut.Free;
   vFileStrm.Free;
  memo1.Lines.Add('PrepareLineForExport_MOVE: ' + vSW.ElapsedMilliseconds.ToString);
end

 

@Kas Ob.How can I export to text file your fastest Binary version?

 

Edited by Mike Torrettinni

Share this post


Link to post

Binary is not text, so it is pointless for your problem. You need the integers to be written as text, not as 4 bytes binary.

 

You could directly write to the TWriteCachedFileStream, too, without any temporary string.
And append the integer values using a shortstring and old str() instead of IntToStr() which uses the heap.

Edited by Arnaud Bouchez

Share this post


Link to post
6 minutes ago, Arnaud Bouchez said:

Binary is not text, so it is pointless for your problem. You need the integers to be written as text, not as 4 bytes binary.

 

You could directly write to the TWriteCachedFileStream, too, without any temporary string.
And append the integer values using a shortstring and old str() instead of IntToStr() which uses the heap.

 

Are suggesting like this?:

 vFileOut.WriteLine(PrepareLineForExport(xData[i]));
 and
 vFileOut.WriteLine(PrepareLineForExport_MOVE(xData[i]));

 

Share this post


Link to post

I forgot to add that I wanted to test this after this post suggesting using MOVE:

 

Of course, in that case it was handling just strings, no integers or other types.

Share this post


Link to post
28 minutes ago, Mike Torrettinni said:

I'm testing with David's Buffered streams

You aren't doing it effectively though, because you are still creating lots of intermediate string objects along the way. Your goal should be to create no intermediate string objects.

 

In my own code which writes huge YAML files containing lots of numbers (both integer and floating point), I export without any heap allocation, without any intermediate string objects. I use functions like this:

 

// disable range checks and overflow checks so that Abs() functions in case Value = Low(Value)
{$R-}
{$Q-}
function CopyIntegerToAnsiBuffer(const Value: Integer; var Buffer: array of AnsiChar): Integer;
var
  i, j: Integer;
  val, remainder: Cardinal;
  negative: Boolean;
  tmp: array [0..15] of AnsiChar;
begin
  negative := Value<0;
  val := Abs(Value);
  Result := 0;
  repeat
    DivMod(val, 10, val, remainder);
    tmp[Result] := AnsiChar(remainder + Ord('0'));
    Inc(Result);
  until val=0;
  if negative then begin
    tmp[Result] := '-';
    Inc(Result);
  end;
  Assert(Result<=Length(Buffer));

  i := 0;
  j := Result-1;
  while i<Result do begin
    Buffer[i] := tmp[j];
    Inc(i);
    Dec(j);
  end;
end;
{$IFDEF RANGECHECKSON}{$R+}{$ENDIF}
{$IFDEF OVERFLOWCHECKSON}{$Q+}{$ENDIF}

This performs the same job as IntToStr but writes to a user supplied buffer, rather than forcing a heap allocation and a new string.  The user supplied buffer is stack allocated, and then gets pushed to the file using one of my buffered stream classes. I'm using ANSI characters here because the output encoding is UTF-8.

 

I have similar code for floating point, but that's obviously much more complex. As an aside, this has the benefit that I can escape the tyranny of the RTL's broken code which converts between floating point and text.

 

In summary though, I don't think you are going about this in the right way. You have not yet correctly identified the bottleneck in your code. I know I keep saying this, but it doesn't make it any less true each time I say it.

Edited by David Heffernan
  • Thanks 1

Share this post


Link to post
Guest
23 minutes ago, Mike Torrettinni said:

How can I export to text file your fastest Binary version?

Not sure if i get the question right.

 

But that will not be a text file, once you want it a full editable text it will lose the performance gain and revert to your version with simple concatenation.

Share this post


Link to post
1 minute ago, Kas Ob. said:

Not sure if i get the question right.

 

But that will not be a text file, once you want it a full editable text it will lose the performance gain and revert to your version with simple concatenation.

Aha, OK, I understand. I need csv file format, so it has to be non-binary text, I guess.

Share this post


Link to post
7 minutes ago, David Heffernan said:

You aren't doing it effectively though, because you are still creating lots of intermediate string objects along the way. Your goal should be to create no intermediate string objects.

 

In my own code which writes huge YAML files containing lots of numbers (both integer and floating point), I export without any heap allocation, without any intermediate string objects. I use functions like this:

 


// disable range checks and overflow checks so that Abs() functions in case Value = Low(Value)
{$R-}
{$Q-}
function CopyIntegerToAnsiBuffer(const Value: Integer; var Buffer: array of AnsiChar): Integer;
var
  i, j: Integer;
  val, remainder: Cardinal;
  negative: Boolean;
  tmp: array [0..15] of AnsiChar;
begin
  negative := Value<0;
  val := Abs(Value);
  Result := 0;
  repeat
    DivMod(val, 10, val, remainder);
    tmp[Result] := AnsiChar(remainder + Ord('0'));
    Inc(Result);
  until val=0;
  if negative then begin
    tmp[Result] := '-';
    Inc(Result);
  end;
  Assert(Result<=Length(Buffer));

  i := 0;
  j := Result-1;
  while i<Result do begin
    Buffer[i] := tmp[j];
    Inc(i);
    Dec(j);
  end;
end;
{$IFDEF RANGECHECKSON}{$R+}{$ENDIF}
{$IFDEF OVERFLOWCHECKSON}{$Q+}{$ENDIF}

This performs the same job as IntToStr but writes to a user supplied buffer, rather than forcing a heap allocation and a new string.  The user supplied buffer is stack allocated, and then gets pushed to the file using one of my buffered stream classes. I'm using ANSI characters here because the output encoding is UTF-8.

 

I have similar code for floating point, but that's obviously much more complex. As an aside, this has the benefit that I can escape the tyranny of the RTL's broken code which converts between floating point and text.

 

In summary though, I don't think you are going about this in the right way. You have not yet correctly identified the bottleneck in your code. I know I keep saying this, but it doesn't make it any less true each time I say it.

 

This probably completely changes my process of export/import csv data. I have 100+ of arrays that I export, so I was testing if MOVE could improve the process of preparing data lines, a little bit. Of course once the data line is in string, the most time consuming is writing to file.

 

 

 

Share this post


Link to post
7 minutes ago, Mike Torrettinni said:

This probably completely changes my process of export/import csv data. I have 100+ of arrays that I export, so I was testing if MOVE could improve the process of preparing data lines, a little bit. Of course once the data line is in string, the most time consuming is writing to file.

Why don't you find out what the bottleneck is first?

 

I simply don't understand why you keep making the same mistakes over and over again.

Share this post


Link to post
1 minute ago, David Heffernan said:

Why don't you find out what the bottleneck is first?

 

I simply don't understand why you keep making the same mistakes over and over again.

What do you think is missing from explanation in first post that would clear the confusion?

 

Share this post


Link to post
6 hours ago, Pierre le Riche said:

Even if you fix that I doubt you'll see more than a marginal performance improvement going from PrepareLineForExport and PrepareLineForExport_MOVE. String concatenation is decently implemented in the RTL. (The 32-bit implementation is even in assembly language.)

 

I think you are right, I would be happy with 10%+ performance gain, but of course 50% would be better for all the extra code needed, vs just +.

Share this post


Link to post
3 minutes ago, Mike Torrettinni said:

What do you think is missing from explanation in first post that would clear the confusion?

 

An explanation of why you start trying to improve performance without first identifying the performance bottleneck.

Share this post


Link to post
9 hours ago, Mike Torrettinni said:

I was trying to compare simple concatenation + vs MOVE to create lines to export, but MOVE is 30% slower than simple +

Is this not clear enough? I would really like to write clearer questions. What is missing here?

Share this post


Link to post

Whether this is in fact your bottleneck in the program.

Share this post


Link to post
7 minutes ago, Mike Torrettinni said:

Is this not clear enough? I would really like to write clearer questions. What is missing here?

What is unclear is why you wrote the code using Move in the first place, since there's no reason to believe that it addresses the performance bottleneck, which you seem not to have identified yet, or even attempted to identify yet.

 

It's not that your questions are unclear, it's that you are asking the wrong questions.

Share this post


Link to post
14 minutes ago, David Heffernan said:

It's not that your questions are unclear, it's that you are asking the wrong questions.

OK, that I understand.

I wanted to see if MOVE gives any benefit for my arrays, mixed field types. When I started I didn't realize how much code is needed vs +. Even though I use code-gen for the whole export/import process, it's a lot of code for arrays with 50+ fields.

But it looks like + is already very performant, or good enough for now.

Share this post


Link to post
7 minutes ago, Mike Torrettinni said:

OK, that I understand.

I wanted to see if MOVE gives any benefit for my arrays, mixed field types. When I started I didn't realize how much code is needed vs +. Even though I use code-gen for the whole export/import process, it's a lot of code for arrays with 50+ fields.

But it looks like + is already very performant, or good enough for now.

It's like we are talking two completely different languages.

  • Haha 1

Share this post


Link to post

The funny thing about this, is that the RTL code that implements a concatenation does this:

  1. Sum the length of all input strings.
  2. Allocate a new string of that length.
  3. Loop through all the input strings, moving their content to the new string.
  4. Return that new string.

So even if this was your bottleneck, the RTL is already optimised!!

 

In that original post that you linked, you were concatenating two strings, but repeatedly in a loop. So you had one allocation per expression. Here you have a single expression concatenating 11 strings. And that also has one allocation, because the RTL is, in this instance, not stupid.

 

Bottom line is that your code using Move should be slower!!

 

Please, please, please, take on board the lesson that optimisation starts with first identifying, with confidence, what your bottleneck is. If you can just take that lesson, then you will save lots of your time (and ours).

Edited by David Heffernan
  • Thanks 1

Share this post


Link to post
1 hour ago, Mike Torrettinni said:

I would really like to write clearer questions. What is missing here?

Less questions. More autonomy.

  • Like 1
  • Thanks 1

Share this post


Link to post
1 hour ago, David Heffernan said:

In that original post that you linked, you were concatenating two strings, but repeatedly in a loop. So you had one allocation per expression. Here you have a single expression concatenating 11 strings. And that also has one allocation, because the RTL is, in this instance, not stupid.

OK, this is something new to me! I never realized this in such a way.

 

Thanks! Everyday something new 😉

Edited by Mike Torrettinni

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×