Jump to content
pyscripter

Smart Pointers - Generics vrs non-generic implementastion

Recommended Posts

Marco Cantu presented in CodeRage 2018 an implementation of Smart Pointers based on generics, similar to the one found in Spring4D. 

 

It can be used as follows:
 

var
  smartP: TSmartPointer<TStringList>;
 begin
   smartP := TStringList.Create;
   smartP.Value.Add('foo');
   TStringList(smartP).Add('foo2');

I am using in my code a non-generics implementation from JclSysUtils

var
  sl: TStringList;
  slSafeGuard: ISafeGuard;
begin
  sl := TStringList(Guard(TStringList.Create, slSafeGuard));
  sl.Add('foo'):

The second has the advantage that you do not have to use the Value property to get the object at the cost of having an extra declaration and being less elegant (?).   

Are there any performance or other advantages to the Generics implementation?  Isn't the non-generics implementation faster and doesn't it generate less code?

 

Edited by pyscripter
Typos

Share this post


Link to post

The ISafeGuard approach is not a smartpointer but creating a scope to some resource. There is a important difference: a smart pointer combines the resource and its lifetime management into one entity.

 

Also the record based approach is the most naive one - while Spring4D offers this one as well it has a more advanced one.

 

As for performance: measure it yourself:

 

program MeasureIt;

{$APPTYPE CONSOLE}

uses
  Spring,
  Diagnostics,
  Classes,
  SysUtils,
  JclSysUtils;

const
  ADD_COUNT = 10;
  CREATE_COUNT = 100000;

procedure MeasureSpring;
var
  s: IShared<TStringList>;
  i: Integer;
begin
  s := Shared.Make(TStringList.Create);
  for i := 1 to ADD_COUNT do
    s.Add(i.ToString);
end;

procedure MeasureJcl;
var
  s: TStringList;
  g: ISafeGuard;
  i: Integer;
begin
  s := TStringList.Create;
  Guard(s, g);
  for i := 1 to ADD_COUNT do
    s.Add(i.ToString);
end;

procedure MeasureClassic;
var
  s: TStringList;
  i: Integer;
begin
  s := TStringList.Create;
  try
    for i := 1 to ADD_COUNT do
      s.Add(i.ToString);
  finally
    s.Free;
  end;
end;


procedure Main;
var
  sw: TStopwatch;
  i: Integer;
begin
  sw := TStopwatch.StartNew;
  for i := 1 to CREATE_COUNT do
    MeasureSpring;
  Writeln(sw.ElapsedMilliseconds);

  sw := TStopwatch.StartNew;
  for i := 1 to CREATE_COUNT do
    MeasureJcl;
  Writeln(sw.ElapsedMilliseconds);

  sw := TStopwatch.StartNew;
  for i := 1 to CREATE_COUNT do
    MeasureClassic;
  Writeln(sw.ElapsedMilliseconds);
end;

begin
  Main;
  Readln;
end.

 

The implementation in Spring 1.2.2 (currently released version) uses very optimized code for the smart pointer itself avoiding the overhead of an object allocation and all the code associated with it but only allocates a 12 Byte (32bit) block with the IMT. Since IShared<T> is not a regular interface but an anonymous method you can directly access the members of the underlying type. Yes, there is a method call every time but as you can measure that does not cause any significant overhead (unless you call .Add a million times). And even then the actual work being performed totally outweighs the smart pointer overhead

Edited by Stefan Glienke
  • Like 4
  • Thanks 2

Share this post


Link to post

As pointed out In another post by @Primož Gabrijelčič, in Delphi Rio you can just write:

  var s := Shared.Make(TStringList.Create);

What an amazing hack Shared is!  Also impressive type inference from Delphi.

Edited by pyscripter
  • Like 2

Share this post


Link to post

And for the record:

Quote

ADD_COUNT: 100  CREATE_COUNT: 100000
      Spring Shared:699
               Jcl: 648
           Classic: 637
     TSmartPointer: 752

 

  • Thanks 1

Share this post


Link to post

One final remark.  If performance is an issue with a "with" statement performance improves to classic levels i.e.

procedure MeasureSpring;
var
  i: Integer;
begin
  var s := Shared.Make(TStringList.Create);
  with s() do
    for i := 1 to ADD_COUNT do
      Add(i.ToString);
end;

 

Share this post


Link to post

@pyscripter You don't need a temp variable at all:

 

procedure MeasureSpring;
var
  i: Integer;
begin
  with Shared.Make(TStringList.Create)() do
    for i := 1 to ADD_COUNT do
      Add(i.ToString);
end;

Just ... with ... brrrr, terrible.

 

This may also work (didn't test):

procedure MeasureSpring;
begin
  // something ...
  begin
    var s := Shared.Make(TStringList.Create)();
    for var i := 1 to ADD_COUNT do
      s.Add(i.ToString);
  end;
  // something ...
end;

I would really like Embarcadero to enhance `with` block in that direction, so I could write:

procedure MeasureSpring;
begin
  with var s := Shared.Make(TStringList.Create)() do
    for var i := 1 to ADD_COUNT do
      s.Add(i.ToString);
end;

This should compile to the same code as my second example. (Except that at the moment it is not support.)

  • Like 1

Share this post


Link to post
35 minutes ago, Primož Gabrijelčič said:

Just ... with ... brrrr, terrible.

Interestingly, that is quite similar to one of the reasons why "with" was invented in the first place: when accessing the record memory is costly. From Pascal User Manual and Report (emphasizes by me):

 

Quote

The with clause effectively opens the scope containing field identifiers of the specified record variable, so that the field identifiers may occur as variable identifiers. (Thereby providing an opportunity for the compiler to optimize the qualified statement.)

 

  • Like 3

Share this post


Link to post

I ran a test in Rio and it works quite well:

type
  TTestObj = class
  public
    constructor Create;
    destructor Destroy; override;
  end;

constructor TTestObj.Create;
begin
  inherited Create;
  Writeln('TTestObj.Create');
end;

destructor TTestObj.Destroy;
begin
  Writeln('TTestObj.Destroy');
  inherited;
end;

procedure Test;
begin
  Writeln('Test started');
  begin var obj := Shared.Make(TTestObj.Create)();
    Writeln('obj = ', obj.ClassName);
    Writeln('End of nested scope');
  end;
  Writeln('Test completed');
end;

Output:

Test started
TTestObj.Create
obj = TTestObj
End of nested scope
Test completed
TTestObj.Destroy

Inline var object is not destroyed at the end of scope, but at the end of the method. Still, that's something I more or less expected. Other than that, the code works fine.

 

For the record, this can also be achieved with the "traditional" syntax. Delphi will kindly keep the interface alive until the end of the method:

procedure Test;
var
  obj: TTestObj;
begin
  Writeln('Test started');
  obj := Shared.Make(TTestObj.Create)();
  Writeln('obj = ', obj.ClassName);
  Writeln('Test completed');
end;

And there's also a bit slower but shorter variation to create a shared object:

Shared<TTestObj>.Make()

Just saying 😉

Edited by Primož Gabrijelčič
  • Like 1
  • Thanks 1

Share this post


Link to post
30 minutes ago, Primož Gabrijelčič said:

Inline var object is not created at the end of scope, but at the end of the method. Still, that's something I more or less expected. Other than that, the code works fine.

 

You are wrong... object is destroyed at the end of the inner scope... but your output  'End of nested scope'  is still executed within inner scope.

Share this post


Link to post
3 minutes ago, Dalija Prasnikar said:

You are wrong... object is destroyed at the end of the inner scope... but your output  'End of nested scope'  is still executed within inner scope.

Nope. Interface is cleared - and object destroyed - in the final `end` of the method. I verified with the debugger.

Share this post


Link to post

@Primož Gabrijelčič  If you do a minor change in Test (remove ())

procedure Test;
begin
  Writeln('Test started');
  begin
    var obj := Shared.Make(TTestObj.Create);
    Writeln('obj = ', obj.ClassName);
    Writeln('End of nested scope');
  end;
  Writeln('Test completed');
end;

Output:

Test started
TTestObj.Create
obj = TTestObj
End of nested scope
TTestObj.Destroy
Test completed

 

Share this post


Link to post

Makes sense.

 

In your code an 'inline var' is an interface of type `IShared<TTestObj>` and is destroyed at the end of the owning scope (begin..end).

 

In my code, the 'inline var' is of type `TTestObj`. The `IShared<TTestObj>` interface is just something that was created during the execution and is owned by the method itself.

 

Share this post


Link to post
2 minutes ago, Primož Gabrijelčič said:

Nope. Interface is cleared - and object destroyed - in the final `end` of the method. I verified with the debugger.

procedure Test;
begin
  Writeln('Test started');
  begin var obj := Shared.Make(TTestObj.Create)();
    Writeln('obj = ', obj.ClassName);
  end;
  Writeln('End of nested scope');
  Writeln('Test completed');
end;

Test with above code, please. I am testing with plain TInterfacedObject and it works as expected...

Share this post


Link to post

@Dalija Prasnikar I think @Primož Gabrijelčičmakes the point that the object is destroyed after 'Test Complete' e.g.

 

procedure Test;
begin
  Writeln('Test started');
  begin
    var obj := Shared.Make(TTestObj.Create)();
    Writeln('obj = ', obj.ClassName);
  end;
  Writeln('End of nested scope');
  Writeln('Test completed');
end;

Output:

Test started
TTestObj.Create
obj = TTestObj
End of nested scope
Test completed
TTestObj.Destroy

I think this is because the result of Shared.Make(TTestObj.Create) which is a reference to function is not explicitly assigned to any variable so its scope is the function scope. The statement

begin var obj := Shared.Make(TTestObj.Create)();

implicitly assigns the result of the function call to the obj whose type is TTestObj.

Edited by pyscripter

Share this post


Link to post
2 minutes ago, pyscripter said:

...is not explicitly assigned to any variable so its scope is the function scope.

 

 

Indeed. That's what I was trying to say four posts back 🙂

Share this post


Link to post
type
  TFoo = class(TInterfacedObject)
  public
    constructor Create;
    destructor Destroy; override;
  end;

constructor TFoo.Create;
begin
  inherited Create;
  Writeln('Foo Create');
end;

destructor TFoo.Destroy;
begin
  Writeln('Foo Destroy');
  inherited;
end;

procedure Test;
begin
  Writeln('procedure begin');
  Writeln('before inner scope begin');
  begin
    Writeln('after inner scope begin');
    var ref: IInterface := TFoo.Create;
    Writeln('before inner scope end');
  end;
  Writeln('after inner scope end');
  Writeln('something');
  Writeln('procedure end');
end;

Results with following output

 

procedure begin
before inner scope begin
after inner scope begin
Foo Create
before inner scope end
Foo Destroy
after inner scope end
something
procedure end

 

Share this post


Link to post
1 minute ago, Primož Gabrijelčič said:

 

Indeed. That's what I was trying to say four posts back 🙂

You are right... implicit variable hell :classic_blush:

 

There were some issues with scoping... so I went to check with simplest example... too simple I am afraid...

Share this post


Link to post

To summarize:

 

Who wants maximum performance like "classic" use it like this

var obj := Shared.Make(TTestObj.Create)();

"obj" is object and will be free it at the end of the procedure;

 

otherwise, use it:

var obj := Shared.Make(TTestObj.Create);

"obj" is interface and will be free it at the end of the block and every access has a small penalty of an anonymous method call.

Edited by Emil Mustea
  • Like 4

Share this post


Link to post

@Emil Mustea You could be that clever however the real runtime overhead of the spring smart pointer is so small that any such tricks are not necessary imo and rather lead to defects because of implicit variables as shown earlier.

 

@pyscripter YMMV but when I compile with release config the results are within 2% difference. Depending on order and CPU caching in that micro-benchmark they are even equal.

  • Thanks 1

Share this post


Link to post
On 12/8/2018 at 9:19 AM, Primož Gabrijelčič said:

I would really like Embarcadero to enhance `with` block in that direction, so I could write:

Why not directly make it `using` and replace the `begin` `end` with `{  }` ?
:)

  • Like 1
  • Haha 1
  • Confused 1
  • Sad 2

Share this post


Link to post
On 12/7/2018 at 5:37 PM, pyscripter said:

Marco Cantu presented in CodeRage 2018 an implementation of Smart Pointers based on generics, similar to the one found in Spring4D. 

 

It can be used as follows:
 


var
  smartP: TSmartPointer<TStringList>;
 begin
   smartP := TStringList.Create;
   smartP.Value.Add('foo');
   TStringList(smartP).Add('foo2');

I am using in my code a non-generics implementation from JclSysUtils


var
  sl: TStringList;
  slSafeGuard: ISafeGuard;
begin
  sl := TStringList(Guard(TStringList.Create, slSafeGuard));
  sl.Add('foo'):

The second has the advantage that you do not have to use the Value property to get the object at the cost of having an extra declaration and being less elegant (?).   

Are there any performance or other advantages to the Generics implementation?  Isn't the non-generics implementation faster and doesn't it generate less code?

 

Heh, I actually donated that SafeGuard code to JCL many years ago. They modified the name a little, but not the concept. It works, but I am not sure if all guarded objects will be released on an exception. You'll have to test that. The advantage is indeed that you don't have to cast or anything. You use the original object you created. Yesterday, I even wrote a better approach, using records an implicit and explict casts. That one is certain to work and does not need a cast and the pointer can be used as if it were the actual object (by implicit cast).

 

type
  IObjectGuard<T: class> = interface
    function Value: T;
  end;

  TObjectGuard<T: class> = class(TInterfacedObject, IObjectGuard<T>)
  private
    FValue: T;
  public
    constructor Create(Obj: T);
    destructor Destroy; override;
    function Value: T;
  end;

  SmartPtr<T: class> = record
  private
    FGuard: IObjectGuard<T>;
  public
    class operator Explicit(const Obj: T): SmartPtr<T>;
    class operator Implicit(const Ptr: SmartPtr<T>): T;
  end;

{ TObjectGuard<T> }

constructor TObjectGuard<T>.Create(Obj: T);
begin
  FValue := Obj;
end;

destructor TObjectGuard<T>.Destroy;
begin
  FValue.Free;
  inherited;
end;

function TObjectGuard<T>.Value: T;
begin
  Result := FValue;
end;

{ SmartPtr<T> }

class operator SmartPtr<T>.Explicit(const Obj: T): SmartPtr<T>;
begin
  Result.FGuard := TObjectGuard<T>.Create(Obj);
end;

class operator SmartPtr<T>.Implicit(const Ptr: SmartPtr<T>): T;
begin
  if Ptr.FGuard <> nil then
    Result := Ptr.FGuard.Value
  else
    Result := nil;
end;

And it can be used like:

 

var
  Bob, Fred, Jack: TTalking;
begin
  Bob := SmartPtr<TTalking>(TTalking.Create('Bob'));
  Fred := SmartPtr<TTalking>(TTalking.Create('Fred'));
  Jack := SmartPtr<TTalking>(TTalking.Create('Jack'));
  Fred.Talk;
  Bob.Talk;
  raise Exception.Create('Error Message');
  Jack.Talk;
  Writeln('This is the end of the routine');
end;

As you can see, this also creates guards. They do work, even with the Exception. All are freed when the exception occurs.

Edited by Rudy Velthuis
  • Like 4

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

×