Jump to content
Marco Cantu

Directions for ARC Memory Management

Recommended Posts

On 10/29/2018 at 10:01 AM, Marco Cantu said:

But would have been even less happy keeping the status quo (2 different models) forever. And we really cannot afford breaking VCL code (billions of lines of working code form our customers)...

It would be nice to have ARC-ON for FMX, and ARC-OFF for VCL. But that means two different runtimes with all the costs.

 

On 10/26/2018 at 2:00 PM, Memnarch said:

If ManagedRecords work as good as what you can do in C++, you can write SmartPointers that enable reference counting when needed.

Records still have reference counting like objects with ARC. So broad use of such SmartPointers will impact performance even more than ARC, because you will need additional memory allocations and so on.

Share this post


Link to post

Personally I am OK with ARC removal, if the main reason is, that it has failed to simplify memory management.

If main reason is performance problems, than it is not OK. Instead of making step back, we should make one step forward, and fix another common programming problem: errors related to multithreading. Something like this: every memory buffer (object, record, string, etc.) should have hidden field, that contains information about which thread owns this buffer. By default the thread, that created the buffer, is its owner. Threads are not allowed to read or write buffers owned by another threads, otherwise RTL will raise "Thread synchronization error". There are special synchronization sections, where threads are allowed to access buffers owned by another threads, for example TThread.Synchronize or code between TCriticalSection.Enter/Leave. Possible optimizations: there can be special procedure, that can recursively change owner of the object and all its fields. Optimization number two: on 64-bit platforms, different threads can use different memory regions, so information about owner thread will be encrypted in the pointer to buffer. Optimization number three: inside synchronization section RTL automatically makes local copy of the string, if it is owned by another thread.

If something like that will be implemented, than there will be no need to protect reference counters with things like InterlockedIncrement, that impact performance so badly.

Edited by Микола Петрівський

Share this post


Link to post
1 hour ago, Микола Петрівський said:

Records still have reference counting like objects with ARC. So broad use of such SmartPointers will impact performance even more than ARC, because you will need additional memory allocations and so on.

Records have no reference counting, and never had

  • Like 1

Share this post


Link to post
2 hours ago, Memnarch said:

Records have no reference counting, and never had

Sorry, I have confused them with strings. But still, record creation will need some time, so SmartPointers will not be free. I hope, we will be able to see some benchmarks on new Linux compiler soon after Rio release.

Share this post


Link to post

Record creation does not take time because they are value types and live on the stack.

If the ctor, dtor and assignment operators are called directly by the compiler and can possibly even be inlined this will have very little overhead which is negectable.

  • Like 1

Share this post


Link to post
1 hour ago, Stefan Glienke said:

Record creation does not take time because they are value types and live on the stack.

But if at least 1 managed field ...

  • Sad 1

Share this post


Link to post
4 hours ago, Stefan Glienke said:

Now it would be cool if on a record type you could specify a property as default so it does member lifting in order to get rid of having to type .Value when accessing the underlying value. Especially when the record does not have any other members anyway. That would indeed make records even more powerful as they are already.

The quote is from:  https://delphisorcery.blogspot.com/2015/01/smart-pointers-in-delphi.html

 

Can somebody confirm that the "new" record type will have a default property? so will not have to write .Value in smart pointers. Indeed this will be really cool and really useful as automatic memory management:

 

var
  s: Shared<TStrings>;
begin
  s := TStringList.Create;
  s.Add('one');
  s.Add('two');
  //here "s" will be realeased automatically because is a record
end;

 

Share this post


Link to post
8 hours ago, Emil Mustea said:

Can somebody confirm that the "new" record type will have a default property?

Afaik it does not. However that is why there is Shared<T> and IShared<T> in Spring. First one is for easy creation because of implicit operator, second is for easy access as its an anonymous method type and the invoke takes almost zero time. With the type inference in 10.3 however the second one also has pretty easy creation as Primoz showed earlier.

  • Like 1

Share this post


Link to post
12 hours ago, Dmitry Arefiev said:

But if at least 1 managed field ...

Then the initalization process is called and it uses RTTI and it is slow, indeed. But it makes no difference in almost any use-case on this planet. (Except for the few cases where it makes a big difference. 🙂 )

Share this post


Link to post

I don't understand what would be wrong to enable ARC for objects and to consider [unsafe] as default. It will not brake old code. And using [safe] or [arc] we can have automatic memory management. At least for simple scenarios.

Share this post


Link to post
2 hours ago, Cristian Peța said:

I don't understand what would be wrong to enable ARC for objects and to consider [unsafe] as default. It will not brake old code. And using [safe] or [arc] we can have automatic memory management. At least for simple scenarios.

It would be wrong in the same way as most of the time it is wrong to mix object and interface references because as soon as interface reference is triggering ARC your instance might be prematurely destroyed.

Edited by Stefan Glienke
  • Like 1

Share this post


Link to post

It would be wrong if somewhere is an unsafe reference. But this is true also if using Shared<T> or IShared<T>. Or any other smart pointer.

Edited by Cristian Peța

Share this post


Link to post

The difference is the implementation, why put something into TObject which is then turned off for everything unless I need it. This is already the case for TMonitor which is kinda arguable. Putting the RefCount field into every single object instance only wastes memory.

So if you are for explicit opt-in it not being part of TObject itself is way better.

 

So if we get records that are not running through InitRecord, CopyRecord and FinalizeRecord but directly call their constructor/destructor/copy operator you get rid of all that overhead and don't need records wrapping interfaces (no heapallocation needed as well). If we then possibly also get a way to do operator lifting/hoisting we get rid of the current need to write .Value when accessing the stored value (which is why I wrote IShared<T> where you don't have to).

  • Like 3

Share this post


Link to post
3 hours ago, Cristian Peța said:

I don't understand what would be wrong to enable ARC for objects and to consider [unsafe] as default. It will not brake old code. And using [safe] or [arc] we can have automatic memory management. At least for simple scenarios.

One of the problems with ARC enabled and having [unsafe] as default is that you cannot release object that has disabled reference counting. [unsafe] is only for marking additional references on objects whose memory is managed at some different place (reference). 

Of course, you can say that compiler would need some tweaking to allow destroying such objects, but it is very likely that at some point the whole construct would fall apart. I never gave mush thought to such "solution" because it seems pointless.

 

Point is. ARC compiler was done right as far as its default behavior is concerned. There are few things around DisposeOf that could be polished, and some other minor bugs that are just bugs, not design flaws.

 

ARC compiler fits the best into existing Delphi infrastructure and neatly fixes issues around object and interface references. Other solutions and workarounds will be poor substitutes and will not solve that duality problem. I wish I would be wrong on this one. I sincerely hope I am wrong and that there is another approach that will not be just a band aid, but full fledged solution.

  • Like 3

Share this post


Link to post

I only wanted a simple way to clear my code of that "try finally end" for local, short lived instances. They are the wast majority in my code. For other objects (the minority) I prefer manual memory management and not to fight an automatic one.

IShared<T> is nice but a little convoluted.

Share this post


Link to post
46 minutes ago, Cristian Peța said:

IShared<T> is nice but a little convoluted

It has the overhead of being an interface, causing heap allocation and doing a call every time you access it. In 1.3 there will be some optimizations to reduce that. The only solution that will not cause all this is the one I described previously.

Share this post


Link to post

Why can't we just write a simple collection of interfaces. That's what I did for some classes.
Take for example TStringList:


 

IARCStringList = Inteface

   procedure Add( aString : string );
   procedure AddStrings( aString : string );
   function AsStringList : TStringList;

   ...
end;

TARCStringList = Class( TInterfacedObject,  IARCStringList  )
private
     FList : TStringList;
public
   // Implement all IStringListARC  methods and properties.
   destructor destroy; // Will call FList.Free;
end;

Usage would be very simple:

procedure DoSomething;
Var 
  S : IARCStringList;
begin
  S := TARCStringList.Create;
  S.Add('One');
  S.Add('Two');
  ListBox1.Items.AddString( S.AsStringList ); // Will return FList

end; 

I wrote for other classes such as TARCDictionary, TARCObjectDictionary, TARCDictionary<K,V>, TARCObjectDictionary<K,O>, etc.

 

I use "const" wherever is required when I use those ARC interfaces.
What problems do you see with such implementations (other than writing a lot of code)?

Share this post


Link to post
On 10/26/2018 at 12:58 PM, Marco Cantu said:

On top of this, we want to improve and simplify the management of the lifetime (and memory management) of local short-lived objects with stack references. This is a not feature we are going to introduce in 10.3, but something we are actively investigating and can be partially implemented by developers leveraging managed records (a new language feature in 10.3).

quote from: http://blog.marcocantu.com/blog/2018-october-Delphi-ARC-directions.html

 

If "managed records" from 10.3 have destructor like I understand, there is no need for default property/member lifting. Will be possible super cool (and readable) construct like this:

begin
	sl := New.Of(TStringList.Create);
   	sl.Add("One");
   	sl.Add("Two");
   	//here the record "New" goes out of scope, runs the it's destructor which will free the instance on which "sl" points to
end;

I hope I'm not missing something.

 

Mr. Marco Cantu, can you confirm the "managed records" from 10.3 have destructor?

Edited by Emil Mustea

Share this post


Link to post
1 hour ago, Emil Mustea said:

I hope I'm not missing something.

Perhaps that New record must also be declared next to s1. Otherwise how can it go out of scope? 

Edited by Cristian Peța

Share this post


Link to post
52 minutes ago, Cristian Peța said:

Perhaps that New record must also be declared next to s1. Otherwise how can it go out of scope? 

Well, New.Of could return a record that has implicit operator overload to be assignable to the s1 variable which then implicitly goes out of scope at the end of the routine.

  • Like 1

Share this post


Link to post

A record without a reference will stay alive up to routine end?  I must test this.

....

Just verified in Lazarus and suppose it's similar in Delphi.

That record lives on stack up to the routine end.

Edited by Cristian Peța

Share this post


Link to post
2 hours ago, Cristian Peța said:

Perhaps that New record must also be declared next to s1. Otherwise how can it go out of scope? 

Sorry, it's my mistake. I forgot the "var": (inline declaration with type inference)

begin
	var sl := New.Of(TStringList.Create);
   	sl.Add("One");
   	sl.Add("Two");
   	//here the record "New" goes out of scope, runs the it's destructor which will free the instance on which "sl" points to
end;

 

1 hour ago, Cristian Peța said:

A record without a reference will stay alive up to routine end?  I must test this.

....

Just verified in Lazarus and suppose it's similar in Delphi.

That record lives on stack up to the routine end.

Better: with inline declaration will stay alive until the block end, not until routine end.

  • Like 1

Share this post


Link to post

New.Of() can also be a simple function that returns a record like:

function Keep(AObject: TObject): TKeepObjectAndFreeRecord;

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

×