Sign in Join

Live chat for Free Pascal, Lazarus, Delphi and every Pascal programmer.

Mailing lists / fpc-pascal / 2012-07

UnicodeString comparison performance

10 messages · 5 people · –

Reply by email Subscribe to this list Your subscription

Read-only archive. Use j and k to move between messages.

OBones

Hello all,

As I'm moving further into my experiments with FreePascal 2.6.0, I have run into a strange issue related to UnicodeString comparisons. Basically, I'm comparing the result of a function with a constant value and find that the comparison is faster if I first assign the result of the function to a local variable. The speed difference is around 25% slower for the version that does a direct comparison. It seems it comes from an unnecessary transliteration in the comparison, but I can't figure out if it's really this and how to circumvent this. I have pasted a program at the end of this message, it is a simplified version of the code I have, but it shows the issue quite well here. Rewriting all code that does that to use a local variable is not desirable, I would very much prefer if there was some sort of compiler option/switch to get similar results in both cases. All this is done using the x86 and x64 windows compilers.

Thanks in advance for your help and patience.

Regards
Olivier
=====================================================

program test;
uses
   SysUtils;
const
   LoopCount = 100000000;
function GetSomeString: UnicodeString;
begin
   Result := 'Tmp';
end;
function Direct: Double; register;
var
   i: Integer;
begin
   Result:= 0;
   for i := 1 to LoopCount do
   begin
     if GetSomeString = 'Z' then
     begin
       Result:= 33;
       Exit;
     end;
   end;
   Result:= 77;
end;
function Indirect: Double; register;
var
   i: Integer;
   S: UnicodeString;
begin
   Result:= 0;
   for i := 1 to LoopCount do
   begin
     S := GetSomeString;
     if S = 'Z' then
     begin
       Result := 33;
       Exit;
     end;
   end;
   Result:= 77;
end;
function TimeToMs(Value: TDateTime): string;
begin
   Result := FormatDateTime('ss.zzz', Value);
end;
procedure DoTest;
var
   StartTime: TDateTime;
begin
   StartTime := Now;
   Direct;
   WriteLn('Direct:   ' + TimeToMs(Now - StartTime));
   StartTime := Now;
   Indirect;
   WriteLn('Indirect: ' + TimeToMs(Now - StartTime));
end;
begin
   DoTest;
end.

Reply by email

leledumbo
to OBones

I look at the generated code and in the direct one there's additional overhead of decrementing the reference counter on each iteration. Here's what the core code looks like:

Direct:
leal -56(%ebp),%eax
call fpc_unicodestr_decr_ref
leal -56(%ebp),%eax
call P$TEST_$$_GETSOMESTRING$$UNICODESTRING
movl -56(%ebp),%eax
movl $_$TEST$_Ld2,%edx
call fpc_unicodestr_compare_equal
testl %eax,%eax

Indirect:
leal -16(%ebp),%eax
call P$TEST_$$_GETSOMESTRING$$UNICODESTRING
movl $_$TEST$_Ld2,%edx
movl -16(%ebp),%eax
call fpc_unicodestr_compare_equal
testl %eax,%eax

View this message in context: http://free-pascal-general.1045716.n5.nabble.com/UnicodeString-comparison-performance-tp5710405p5710408.html Sent from the Free Pascal - General mailing list .

Reply by email

OBones
to leledumbo
leledumbo wrote
I look at the generated code and in the direct one there's additional overhead of decrementing the reference counter on each iteration.

I see it too now (I forgot about the -a option).
I can understand why there is a call to the decrementer outside the loop when using the variable, but I don't understand why it pushes it completely out of the loop. I mean, isn't there a reference count problem here?
This is all very strange to me...

Reply by email

Jonas Maebe
to OBones: I see it too now (I forgot about the -a option). I can understand why there is a call to…
OBones wrote 5 lines
leledumbo wrote: > I look at the generated code and in the direct one there's additional > overhead of decrementing the reference counter on each iteration. I see it too now (I forgot about the -a option). I can understand why there is a call to the decrementer outside the loop when using the variable, but I don't understand why it pushes it completely out of the loop. I mean, isn't there a reference count problem here?

Reference counted data types are returned by reference in a location passed to the function by the caller. The compiler here passes the address of S to the function, so that when assigning something to the function result inside the function, S' reference count gets decreased.

The fact that when the result is returned in a temp, this temp also gets finalized on the caller side before the call is just a code generator inefficiency. I've changed that in trunk.

Jonas

Reply by email

OBones
to Jonas Maebe
Jonas Maebe wrote 10 lines
On 23 Jul 2012, at 10:58, OBones wrote: > leledumbo wrote: >> I look at the generated code and in the direct one there's additional >> overhead of decrementing the reference counter on each iteration. > I see it too now (I forgot about the -a option). > I can understand why there is a call to the decrementer outside the loop when using the variable, but I don't understand why it pushes it completely out of the loop. I mean, isn't there a reference count problem here? Reference counted data types are returned by reference in a location passed to the function by the caller. The compiler here passes the address of S to the function, so that when assigning something to the function result inside the function, S' reference count gets decreased. The fact that when the result is returned in a temp, this temp also gets finalized on the caller side before the call is just a code generator inefficiency. I've changed that in trunk.

Thanks for the explanation and the fix.
In my example the difference is very small, but in my real code, it was amplified by the fact that I was doing this:

   SetLength(Result, 1024 * 1024);
   CallToAnAPIThatWritesBackAWideString(@Result[1], Length(Result) - 1);
   SetLength(Result, StrLen(PWideChar(Result)));

Because the finalization happened too early, those memory allocations and deallocations were very costly and I found the direct code to be 30 times slower.
Doing it this way has the advantage of being inherently thread safe, but considering the performance penalty, I have moved to doing it this way:

var
   Buffer: array [0..1024*1024-1] of WideChar;
   BufferLock: NativeInt;
function GetSomeString(Index: Integer): UnicodeString;
begin
   while BufferLock > 0 do
     Sleep(1);
   InterlockedIncrement(BufferLock);
   try
     CallToAnAPIThatWritesBackAWideString(@Buffer[0], Length(Buffer) - 1);
     Result := PWideChar(@Buffer[0]);
   finally
     InterlockedDecrement(BufferLock);
   end;
end;

This works, with an equivalent penalty on both methods and is threadsafe (I believe).

Should anyone have a better workaround, I'd be very pleased to hear about it as I can't move to the trunk version of FPC just yet.

Many thanks for the help

Regards

Reply by email

Thomas Schatzl
to OBones

HI,

OBones wrote 39 lines
Jonas Maebe wrote: > On 23 Jul 2012, at 10:58, OBones wrote: > >> leledumbo wrote: >>> I look at the generated code and in the direct one there's additional >>> overhead of decrementing the reference counter on each iteration. >> I see it too now (I forgot about the -a option). >> I can understand why there is a call to the decrementer outside the loop when using the variable, but I don't understand why it pushes it completely out of the loop. I mean, isn't there a reference count problem here? > Reference counted data types are returned by reference in a location passed to the function by the caller. The compiler here passes the address of S to the function, so that when assigning something to the function result inside the function, S' reference count gets decreased. > > The fact that when the result is returned in a temp, this temp also gets finalized on the caller side before the call is just a code generator inefficiency. I've changed that in trunk. Thanks for the explanation and the fix. Because the finalization happened too early, those memory allocations and deallocations were very costly and I found the direct code to be 30 times slower. Doing it this way has the advantage of being inherently thread safe, but considering the performance penalty, I have moved to doing it this way: var Buffer: array [0..1024*1024-1] of WideChar; BufferLock: NativeInt; function GetSomeString(Index: Integer): UnicodeString; begin while BufferLock > 0 do Sleep(1); InterlockedIncrement(BufferLock); try CallToAnAPIThatWritesBackAWideString(@Buffer[0], Length(Buffer) - 1); Result := PWideChar(@Buffer[0]); finally InterlockedDecrement(BufferLock); end; end; This works, with an equivalent penalty on both methods and is threadsafe (I believe).

This code is not thread thread switch after the while loop and before the increment will not prevent progress on other threads, so multiple threads can enter the "critical section".

Use EnterCriticalSection/LeaveCriticalSection from the RTL if you need that.

If you could somewhat decrease the size of your buffer to something reasonable - do you really expect to translate a string with 1M chars? - use the stack. Actually even a 2M data object on the stack might be reasonable.

Further, in any case you'll need some code that works in "all" cases anyway - what if your string does exceed 1M chars after all? I mean, on a 1M string, heap allocation will probably be the least of your performance worries.

This will completely avoid use of any synchronization primitive.

E.g.

function GetSomeString(index : integer) : UnicodeString;
const
  bufsize = 1024; // as you wish
var
  buffer : array[0..bufsize-1] of WideChar;
  pbuffer : PWideChar;
  islongstring : boolean;
  lengthofstring : integer;
begin
  lengthofstring := length(stringfromindex(integer));
  islongstring := lengthofstring > bufsize;
  if (islongstring) then
    pbuffer := getmem(lengthofstring * sizeof(WideChar));
  else
    pbuffer := @buffer;
  CallToAnAPIThatWritesBackAWideString(stringfromindex(integer),
pbuffer, lengthofstring);
  if (islongstring) then
    freemem(pbuffer);
  result := pbuffer;
end;

You may want to improve the code further by e.g. checking whether the memory allocation has been successful or not; or factor out the case when the string is big into a separate method to improve readability.

Thomas

Reply by email

OBones
to Thomas Schatzl
Thomas Schatzl wrote
This code is not thread thread switch after the while loop and before the increment will not prevent progress on other threads, so multiple threads can enter the "critical section".

Well, yes, even if under Windows it's rare that a thread is preempted right in the middle of its execution. At least that's what my own experience has told me and why I'm calling Sleep in the while loop, so that the thread yields to other ones.

Use EnterCriticalSection/LeaveCriticalSection from the RTL if you need that.

I did not because I thought that would be too slow, but it turns out it's not so yes, it's much better.

If you could somewhat decrease the size of your buffer to something reasonable - do you really expect to translate a string with 1M chars? - use the stack. Actually even a 2M data object on the stack might be reasonable.

That's the highest value that the documentation of the API is giving, so I guess the API will respect its own publicized contract.

Show quoted text 21 lines
function GetSomeString(index : integer) : UnicodeString; const bufsize = 1024; // as you wish var buffer : array[0..bufsize-1] of WideChar; pbuffer : PWideChar; islongstring : boolean; lengthofstring : integer; begin lengthofstring := length(stringfromindex(integer)); islongstring := lengthofstring > bufsize; if (islongstring) then pbuffer := getmem(lengthofstring * sizeof(WideChar)); else pbuffer := @buffer; CallToAnAPIThatWritesBackAWideString(stringfromindex(integer), pbuffer, lengthofstring); if (islongstring) then freemem(pbuffer); result := pbuffer; end;

Thanks for your example, but it implies that the string length is known before calling the API while this is not the case. The API is given a buffer and a maximum length, a buffer into which it writes a zero terminated string. But the length of that string cannot be known beforehand, hence the reason why I wrote it that way. Note also that having buffer local to the function is just as bad (performance wise) as setting the length of the result string.

In the end, many thanks for your suggestions, I'll go with the critical section as it is much safer.

Regards
Olivier

Reply by email

Sven Barth
to OBones
OBones wrote 8 lines
Thomas Schatzl wrote: > This code is not thread thread switch after the while > loop and before the increment will not prevent progress on other > threads, so multiple threads can enter the "critical section". Well, yes, even if under Windows it's rare that a thread is preempted right in the middle of its execution. At least that's what my own experience has told me and why I'm calling Sleep in the while loop, so that the thread yields to other ones.

Windows will preempt a thread once it's time slice has run out or it yields it's execution. In the first case it does not matter whether the thread is inside some function execution, because the sheduler does not know about such a concept.

Regards,
Sven

Reply by email