System.Move
works with untyped pointers and counter of bytes. System.Copy
and SysUtils.StrLCopy
work with strings (Pascal strings and C strings respectively) and counter of chars. But char and byte are different types, so when you move from string/char context to pointers/bytes context - you should re-calculate length in chars to length in bytes. By the way, same is about indices, Result [Temp1]
calculates in characters, not in bytes. And always did.
Correct solution is not mixing citizens of different planets. If you want pointers - use pointers. If you want characters and strings - use characters and strings. But do not mix them! divide and conquer and always separate and make clear when you're using raw piinters and where you use typed strings! Otherwise you're misleading yourself;
function StringListToDelimitedString
( const AStringList: TStrings; const ADelimiter: String ): String;
var
Str : array of String;
Lengths : array of Integer;
Temp1 : NativeInt;
Count, TotalChars : Integer;
PtrDestination: PByte;
PCurStr: ^String;
CurLen: Integer;
Procedure Add1(const Source: string);
var count: integer; // all context is in bytes, not chars here!
Ptr1, Ptr2: PByte;
begin
if Source = '' then exit;
Ptr1 := @Source[ 1 ];
Ptr2 := @Source[ Length(Source)+1 ];
count := ptr2 - ptr1;
Move( Source[1], PtrDestination^, count);
Inc(PtrDestination, count);
end;
begin // here all context is in chars and typed strings, not bytes
Count := AStringList.Count;
if Count <= 0 then exit('');
SetLength(Str, Count); SetLength(Lengths, Count);
TotalChars := 0;
for Temp1 := 0 to Count - 1 do begin
PCurStr := @Str[ Temp1 ];
PCurStr^ := AStringList[ Temp1 ]; // caching content, avoiding extra .Get(I) calls
CurLen := Length ( PCurStr^ ); // caching length, avoind extra function calls
Lengths[ Temp1 ] := CurLen;
Inc(TotalChars, CurLen);
end;
SetLength ( Result, TotalChars + ( Count-1 )*Length( ADelimiter ) );
PtrDestination := Pointer(Result[1]);
// Calls UniqueString to get a safe pointer - but only once
for Temp1 := Low(Str) to High(Str) do
begin
Add1( Str[ Temp1 ] );
Dec( Count );
if Count > 0 // not last string yet
then Add1( Delimeter );
end;
end;
Now, the correct solution i believe would be stopping inventing bicycles and using ready-made and tested libraryes, for example.
Str := JclStringList().Add(['Hello1','Hello2','Hello3','Hello4']).Join(';');
Or, if you really need to add a delimiter PAST THE LAST string (which is usually carefully avoided) then
Str := JclStringList().Add(['Hello1','Hello2','Hello3','Hello4', '']).Join(';');
The original claim of squeezing single percents of CPU power just does not hold with the original code. The illusion of fast pointer operations is just shadowed by a suboptimal code caring not about performance at all.
function StringListToDelimitedString
( const AStringList: TStringList; const ADelimiter: String ): String;
TStringList
is a class. Class instance creation and deletion are expensive (slow) operations. Delphi made a flexible framework of those classes - but the speed suffers. So if you want to get few extra percents of speed and pay with sacrificing reliability and flexibility - the don't use classes.
DelimiterSize : Byte;
It should be NativeInt
instead just as the rest of muneric variables there. You think you just saved few bytes - but you forced CPU to use non-native datatype and insert typecasts every now and then. It is nothing but an explicitly introduced delay. Ironically, you even did not saved those bytes, cause Delphi would just pad three bytes more to allocate next variable on 32-bits boundary. That is a typical "memory alignment" optimization.
Result := ' ';
This value would never be used. So it is just a loss of time.
for Str in AStringList do
This construction, requiring instantiating TInterfacedObject
and calling its virtual methods and then reference-counting it with global locking - is expensive (slow) operation. And twice slow in multithreading taskloads. If you need to squeeze few percetns of speed - you should avoid loosing tens of percents on for-in loops. Those hi-level loops are handy and reliable and flexible - but they pay with speed for that.
for Str in AStringList do
Then You do it twice. But you DON'T KNOW how it that stringlist implemented. How efficiently does it gets the string ? It may even pass messages to another process like TMemo.Lines do! So you should minimize all accesses to that class and its multitude of internal virtual members. Cache all the strings ONCE in some local variable, do not fetch TWICE every of those!
Move ( Str [1], Result [Temp1], Temp2 );
Now we come to a really interesting question - is there even hypothetical place to gain any speed advantage by usiong pointers and bytes? Open CPU window and look how that line is actually implemented!
Strings are reference-counted! When you do Str2 := Str1;
no data is copied but only pointers do. But as you start accessing the real memory buffer inside the string - that Str[1]
expression - the compiler can not more count references, so Delphi is forced here to decrease reference coutner to ONE SINGLE. That is, Delphi is forced here to call UniqueString
over Str
and over Result
; the System.UniqueString
checks the refcounter and if it is >1 makes a special local copy of string (copying all the data into a newly allocated special buffer). Then you do a Move
- just like Delphi RTL does itself. I cannto get where any advantage of speed may come from?
Move ( ADelimiter [1], Result [Temp1], DelimiterSize )
And here the same operations are done yet again. And they are costly operations! At least an extra procedure is called, at worst the new buffer is allocated and all the content being copied.
Resume:
The boundary between reference-counted strings and raw pointers is a costly one and every time you cross it - you force Delphi to pay a price.
Mixing those boundaries in the same code make the price paid again and again and again. It also confuses yourself where your counters and indices refer to bytes and where they refer to chars.
Delphi optimized casual string operations for years. And did a pretty good job there. Outperforming Delphi is possible - but you would need to understand in very very fine details - up to each CPU assembler instruction - what goes under the curtains of Pascal sources in your program. That is a dirty and tedious work. There will be no mopre that luxury of using those reliable and flexible things as for-in loop and TStrings classes.
In the end you would most probably get a few percents of speed gain, that no one would ever notice. But you will pay for that with a code much harder to understand, write, read and test. Will those few percents of speed worth unmaintainable code ? I doubt it.
So unless you are forced to do so, my advice is to skip wasting your time and just do a usual Str := JclStringList().Add(['Hello1','Hello2','Hello3','Hello4']).Join(';');
Reliability and flexibility is almost always more preferable than sheer speed.
And sorry to tell that, while i do not know a lot about speed optimizations, i easily saw a speed-damaging issues in your code, that you intended to be faster than Delphi itself.
My experience is miles and miles away of even trying to outperform Delphi in strings field. And i do not think you have any chances other but wasting a lot of time to finally get performance worse than stock one.