Recent

Author Topic: Need help to understand the operator overloading implementation in FPC  (Read 1008 times)

MathMan

  • Sr. Member
  • ****
  • Posts: 405
Hello all,

This needs some initial explanation, so pls bear with me.

Some years ago I did some experiments with 'standard' operator overloading (oo) under iirc FPC 3.0.4. 'Standard' in this case means having a separate structure and implementing oo like e.g. (from my 128 bit integer unit)

Code: Pascal  [Select][+][-]
  1.   operator +( constref O1: tUInt128; constref O2: tUInt128    ) Sum: tUInt128;
  2.  

At that time I discovered that I had to be carefull when implementing the oo, as the compiler directly mapped the result type ('Sum' in above) to the left hand side of an expression. This could lead to incorrect results, if not implemented carefully, in expressions like e.g.

Code: Pascal  [Select][+][-]
  1.   A := A + B; // A, B of type tUInt128
  2.  

I verified this, at the time, by looking at the compiler generated asm - even including some notes about it in my sources.

Recently I have reverted to that topic (under FPC 3.2.2) and took my old Int128 unit up again to implement some neglected topics and also started, due to a hint from Bart, some experiments with operator overloading in advanced records, like e.g.

Code: Pascal  [Select][+][-]
  1.   class operator :=( const X: UInt8  ):tSpecial;
  2.  

I made the advanced record a managed object by implementing 'Initialize' & 'Finalize' methods.

In both cases it now looks like the compiler is implementing a hidden, intermediate result variable, from which the result is copied back to the left hand side of an expression. I detected that as I saw more calls to 'Initialize' & 'Finalize' then I would have expected from used variables alone (on the calling side).

Now I have some questions re the above

 - am I correct that something has changed re oo between FPC 3.0.4 and 3.2.2?
 - am I correct regarding that hidden variable?
 - if the above is correct, does that force me to also implement 'Copy' if I made an advanced record a managed object via 'Initialize' & 'Finalize'? Especially if the record contains a pointer to data on the heap, like

Code: Pascal  [Select][+][-]
  1.   private
  2.     A: UInt32;
  3.     B: UInt32;
  4.     C: UInt32;
  5.     D: pUInt64;
  6.   public
  7.  

Kind regards,
MathMan

jamie

  • Hero Member
  • *****
  • Posts: 6733
Re: Need help to understand the operator overloading implementation in FPC
« Reply #1 on: November 09, 2024, 08:50:18 pm »
Looks like Initialize is getting called multiple times, one for the use of the variable and one for each time its found in the sum string.
 :o
The only true wisdom is knowing you know nothing

Thaddy

  • Hero Member
  • *****
  • Posts: 16138
  • Censorship about opinions does not belong here.
Re: Need help to understand the operator overloading implementation in FPC
« Reply #2 on: November 10, 2024, 08:15:20 am »
Initialize is only callled multiple times if inside the initialize call Default() is used or system.initialize is called.
My example in the wiki was until recently wrong in that respect, but is corrected with an explanation after PascalDragon showed us that effect.
Basically if the same behavior as Default() is required you should just use fillbyte/fillchar. This is what Default does, but Default also calls the management operator, causing the multiple calls. The same is true for calling system.initialize. These calls should be avoided inside the management operators as should system.finalize inside the finalize operator. My example in the wiki shows now correctly that the management operator is called only once.(initialization is done with fillbyte)
The example now reads:
Code: Pascal  [Select][+][-]
  1. {$mode objfpc}{$modeswitch advancedrecords}
  2. type
  3.   array10 = array[0..9] of byte;
  4.  
  5.   TTest = record
  6.   fields:array10;
  7.   class operator initialize(var value:TTest);
  8.   end;
  9.  
  10.   class operator ttest.initialize(var value:TTest);
  11.   begin
  12.     // sizeof takes only the fields of the record in to account
  13.     fillbyte(value,sizeof(TTest),0);// do not use default(), that triggers initialize twice
  14.   end;
  15.  
  16. procedure testme;
  17. var
  18.   a,b:TTest;// stack
  19.   i:integer;
  20. begin
  21.   a.fields:=[1,2,3,4,5,6,7,8,9,10];
  22.   a:=b;
  23.   for i :=0 to 9 do
  24.     write(a.fields[i]:3);
  25. end;
  26.  
  27. begin
  28.   testme;
  29. end.
This example writes all zero's. It uses local variables, because they are on the stack and the stack is dirty, hence a better way to demonstrate the initialize operator, since Heap memory is always initialized.

I suppose your Uint128 is a packed record of two qwords. In that case this is enough:
Code: Pascal  [Select][+][-]
  1. {$mode objfpc}{$modeswitch advancedrecords}
  2. type
  3.  TUint128 = packed record
  4.  lo,hi:Qword;
  5.  class operator initialize(var value:TUint128);
  6.  end;
  7.  
  8.  class operator TUint128.initialize(var value:TUint128);
  9.  begin
  10.    // do not call default() inside management operators
  11.    Value.lo := 0;
  12.    Value.Hi := 0;
  13.  end;
  14.  
  15. type
  16.  // cascading initialize....
  17.  TUint256 = record
  18.  lo,hi:TUint128;
  19.  end;
  20.  
  21.  TUint512 = record
  22.  lo,hi:TUint256;
  23.  end;
  24.  
  25. begin
  26. end.
The latter two do not need a management operator, since initializing cascades over from TUint128. It could possibly even lead to the same issue as described above, when you are not careful.(actually most mistakes I see with management operators are a manifestion of this)
Further,
- yes there is a difference between the versions
- you need the copy operator (assignment operator) to provide a deep copy when using :=
Warning the copy operator has the same behavior, so also cascades for 256 and 512.
« Last Edit: November 10, 2024, 09:42:12 am by Thaddy »
If I smell bad code it usually is bad code and that includes my own code.

MathMan

  • Sr. Member
  • ****
  • Posts: 405
Re: Need help to understand the operator overloading implementation in FPC
« Reply #3 on: November 10, 2024, 09:44:02 am »
@Thaddy,

Thanks for the explanation. This is the same as from the wiki page https://wiki.freepascal.org/management_operators (obviously  :D) that I had studied carefully.

However I am at a loss to explain the output of the attached, without assuming that there is a lot of hidden workmanscraft from the compiler going on. It gets even more interesting if you change the constant

Code: Pascal  [Select][+][-]
  1.   ASSIGN_COUNT := 10;
  2.  

to

Code: Pascal  [Select][+][-]
  1.   ASSIGN_COUNT := 100;
  2.  

Any explanation or ideas what I possibly have done wrong, or how to reduce the internal compiler generated operations? I'm working with FPC 3.2.2 as mentioned earlier.

Cheers,
MathMan

Thaddy

  • Hero Member
  • *****
  • Posts: 16138
  • Censorship about opinions does not belong here.
Re: Need help to understand the operator overloading implementation in FPC
« Reply #4 on: November 10, 2024, 09:44:46 am »
To show you the effects look at this slightly modified example:
Code: Pascal  [Select][+][-]
  1. {$mode objfpc}{$modeswitch advancedrecords}
  2. type
  3.  TUint128 = record
  4.  lo,hi:Qword;
  5.  class operator initialize(var value:TUint128);
  6.  class operator copy(constref a:TUint128;var b:TUint128);
  7.  end;
  8.  
  9.  class operator TUint128.initialize(var value:TUint128);
  10.  begin
  11.    Value.lo :=0;
  12.    Value.Hi := 0;
  13.    writeln('ínitialize');
  14.  end;
  15.  
  16.  class operator TUint128.copy(constref a:TUint128;var b:TUint128);
  17.  begin
  18.    b.lo := a.lo;
  19.    b.hi := a.hi;
  20.    writeln('copy called');
  21. end;
  22.  
  23. type
  24.  // cascading initialize....
  25.  TUint256 = record
  26.  lo,hi:TUint128;
  27.  end;
  28.  
  29.  TUint512 = record
  30.  lo,hi:TUint256;
  31.  end;
  32. var
  33.  a,b:TUint512;
  34. begin
  35.  // in total 8 times initialize
  36.  // and 4 times copy
  37.  a:=b;
  38. end.
If I smell bad code it usually is bad code and that includes my own code.

MathMan

  • Sr. Member
  • ****
  • Posts: 405
Re: Need help to understand the operator overloading implementation in FPC
« Reply #5 on: November 10, 2024, 11:04:07 am »
Thaddy,

Pls take a look at the program I attached to my first response - you might have missed it, our posts crossed by mere seconds.

It adheres to what you presented, and what is stated on the wiki about management operators - still it produces some very strange output.

Cheers,
MathMan

Thaddy

  • Hero Member
  • *****
  • Posts: 16138
  • Censorship about opinions does not belong here.
Re: Need help to understand the operator overloading implementation in FPC
« Reply #6 on: November 10, 2024, 11:19:00 am »
That is immediately obvious - at least to me - and just like my last example describes:
If you implement the initialize operator or the copy operator in the smallest possible record, you must NOT override the assignment operators, at least not all of them, or the other way around and leave out the copy operator. Management operators have precedence over all other operators, they are always called. That explains that even though the assignment operators seem completely implemented afresh. copy is still called... What I was trying to warn against.
You fell into the common trap.. :o
I tested your code same way as my last example, with a couple of writeln's.
You can see what happens then.. You can solve this in two ways, leave out the copy and implement only assignments directly or depending on your requirements just implement copy alone in the lowest common denominator record. You can see the cascading happen with your code too. The former is more work for possibly more speed, the latter is shorter at the cost of possibly loss of speed.
( for good measure, my 512 type calls the 128 type four times for every declaration or assignment.)
« Last Edit: November 10, 2024, 11:35:42 am by Thaddy »
If I smell bad code it usually is bad code and that includes my own code.

MathMan

  • Sr. Member
  • ****
  • Posts: 405
Re: Need help to understand the operator overloading implementation in FPC
« Reply #7 on: November 10, 2024, 11:47:58 am »
That is immediately obvious - at least to me - and just like my last example describes:
If you implement the initialize operator or the copy operator in the smallest possible record, you must NOT override the assignment operators, at least not all of them, or the other way around and leave out the copy operator. Management operators have precedence over all other operators, they are always called. That explains that even though the assignment operators seem completely implemented afresh. copy is still called... What I was trying to warn against.
You fell into the common trap.. :o
I tested your code same way as my last example, with a couple of writeln's.
You can see what happens then.. You can solve this in two ways, leave out the copy and implement only assignments directly or depending on your requirements just implement copy alone in the lowest common denominator record. You can see the cascading happen with your code too. The former is more work for possibly more speed, the latter is shorter at the cost of possibly loss of speed.
( for good measure, my 512 type calls the 128 type four times for every declaration or assignment.)

Hm - I hope I understand what you tried to convey to me (I still consider myself as a noob re operator overloading). I'll try to modify my implementation in the the first way you mentioned, as my overarching target is efficiency. I'll keep you posted on progress.

Thaddy

  • Hero Member
  • *****
  • Posts: 16138
  • Censorship about opinions does not belong here.
Re: Need help to understand the operator overloading implementation in FPC
« Reply #8 on: November 10, 2024, 11:51:43 am »
Read "- at least to me -" as at the cost of a long period of headache.... Which I tried to prevent happening to you.... O:-)
If I smell bad code it usually is bad code and that includes my own code.

MathMan

  • Sr. Member
  • ****
  • Posts: 405
Re: Need help to understand the operator overloading implementation in FPC
« Reply #9 on: November 10, 2024, 12:42:29 pm »
Don't worry too much about headache on my side ;)

I already verified that leaving out the copy operator is simply not possible in the approach with advanced records, as I can not make good this ommission by implementing an assignment operator for equal types which should do the deep copy instead. Compiler simply refuses.

If I only remove the copy operator, then assignments like

Code: Pascal  [Select][+][-]
  1.   Arb1 := Arb2;
  2.  

are only shallow copies and I loose the associated heap.

So for me this now boils down to - I can implement the way I started but I get massive overhead for the improved/simplified use on the application side. I might follow this route, but something decent/fast is not achievable this way. BTW I now understand much better where the implementation of ad1mt is unexpectedly loosing efficiency.

If you think I still misunderstood then pls provide a modification of my attached Pascal sources to show me concrete how I should implement things. For me it would be sufficient if you add some comments like 'delete this and that', 'add this', 'modify this to ...' - I will do the actual coding then and get back to you.
« Last Edit: November 10, 2024, 12:57:28 pm by MathMan »

Thaddy

  • Hero Member
  • *****
  • Posts: 16138
  • Censorship about opinions does not belong here.
Re: Need help to understand the operator overloading implementation in FPC
« Reply #10 on: November 10, 2024, 12:58:36 pm »
Can you give another example of your last try? I am - almost - sure I can make it work. The deep copy is not a problem when you just override the := a couple of times. Will add example in a couple of minutes. The ratio is that you only can not assign equal types.(which already makes a deep copy)
« Last Edit: November 10, 2024, 01:11:54 pm by Thaddy »
If I smell bad code it usually is bad code and that includes my own code.

MathMan

  • Sr. Member
  • ****
  • Posts: 405
Re: Need help to understand the operator overloading implementation in FPC
« Reply #11 on: November 10, 2024, 01:07:05 pm »
Can you give another example of your last try? I am - almost - sure I can make it work. The deep copy is not a problem when you just override the := a couple of times. Will add example in a couple of minutes. The ratio is that you only can not assign equal types.

Sure, pls find attached.

Thaddy

  • Hero Member
  • *****
  • Posts: 16138
  • Censorship about opinions does not belong here.
Re: Need help to understand the operator overloading implementation in FPC
« Reply #12 on: November 10, 2024, 01:21:40 pm »
Maybe arbuint and arbsigned?
For unsigned int I have got it working with a simple move, but scratching my head over the sign.
That should be a simple mask with sar and subs. a shl over the size.
If I smell bad code it usually is bad code and that includes my own code.

MathMan

  • Sr. Member
  • ****
  • Posts: 405
Re: Need help to understand the operator overloading implementation in FPC
« Reply #13 on: November 10, 2024, 01:42:47 pm »
Maybe arbuint and arbsigned?
For unsigned int I have got it working with a simple move, but scratching my head over the sign.
That should be a simple mask with sar and subs. a shl over the size.

Actually no. The fastest way I found is indeed the 'UInt8( X<0 )' mantra I implemented - and why I declared Sign: UInt8 in the record.

Warfley

  • Hero Member
  • *****
  • Posts: 1734
Re: Need help to understand the operator overloading implementation in FPC
« Reply #14 on: November 10, 2024, 02:26:45 pm »
Fpcs management operators are slow. First they are implemented using RTTI, so they are always dynamic function pointer calls, just look at the code in fpcsrc/RTL/inc/rtti.inc, there is a lot of runtime stuff going on, which takes some time.

Second they are called very often. Each function call has their own managed scope for result, and it is passed between function calls through copy. The former is intended to ensure scoping is valued, the latter is a result of the missing move/swap operation. C++ for example has a move assignment, allowing to move the contents from one data structure to another and clearing the first one, which is incredibly useful for temporary objects like those returned by a construction function, to not have to do a deep copy but can just grab the pointer and clear

Right now you can emulate that by adding another field indicating whether or not it is a temporary object, set it in each of for functions for the return value, and if so don't do a deep copy but just a move

Code: Pascal  [Select][+][-]
  1. TMyRec = record
  2.   Data: Pointer;
  3.   IsTemp: Boolean;
  4.   ...
  5. end;
  6.  
  7. class operator TMyRec.Copy(constref src: TMyRec; var dst: TMyRec);
  8. begin
  9.   If src.isTemp then
  10.   begin
  11.     dst.Data:=src.Data;
  12.     Fillchar(PByte(@src)^,sizeof(src),#00);
  13.   end
  14.   else
  15.     // Deepcopy
  16. end;

Note: as you can see I cast the const away which is very bad practice, but it's a practical solution that works right now

 

TinyPortal © 2005-2018