Autorelease Pool

Manual memory management requires some thought and ceremony. It is not so much of a problem with long-lasting instances, but managing temporary local objects, especially when you need to create more than one, is a tedious job. It also hurts code readability.

While Delphi allows automatic memory management for classes that implement interfaces, using ARC is not always feasible. If you have to deal with preexisting classes that don't support ARC, you will have to deal with manual memory management. Reference counting also adds some overhead, and using reference counting in your own classes is not always a viable approach.

When performance is not paramount, simpler and cleaner code without try...finally blocks would be nice to have. Life always finds a way and various ARC-based smart pointer and similar lifetime management wrapper implementations have started to appear in the wild.

One common place where constructing many object instances can be found is unit testing. While lifetime wrappers work well in applications, they often wrap object instances too well, and that can create issues in unit testing.

There is another approach to lifetime management that does not require wrapping the managed object - using some kind of auto-release pool that will take ownership (and memory management) over object instances, while keeping the original reference intact. In the case of single objects, there is no need for a pool and maintaining the list.

Implementing such auto-release managers is rather simple. Managers on their own are reference-counted: Their memory is automatically managed, and will be released when they go out of scope. To ensure proper construction and reference counting initialization for the TAutoRelease and TAutoReleasePool classes, their declaration will be hidden in the implementation section of a unit, and they can be constructed only through the AutoRelease record's static functions.

The TAutoRelease class can manage the lifetime of a single object instance. Since the compiler maintains a hidden interface reference in the method scope, there is no need for declaring an additional variable.

procedure TestXXX.TestYYY; var Foo: TFoo; begin Foo := TFoo.Create; AutoRelease.New(Foo); ... // use Foo end; // The TAutoRelease object will be destroyed at this point, and will release Foo

The TAutoReleasePool class can manage the lifetime of multiple object instances. To use a pool, we need to keep a reference to the pool. In a unit testing environment, the pool can be a field in the test class that will be created and destroyed for every test in the SetUp and TearDown methods:

TestXXX = class(TTestCase) strict private AutoPool: IAutoReleasePool; ... procedure TestXXX.SetUp; begin inherited; AutoPool := AutoRelease.NewPool; end; procedure TestXXX.TearDown; begin AutoPool := nil; inherited; end; procedure TestXXX.TestYYY; var Foo: TFoo; Bar: TBar; begin Foo := TFoo.Create; AutoPool.Add(Foo); Bar := TBar.Create; AutoPool.Add(Bar); ... // use Foo and Bar end;

Using such auto-release managers is not limited to a unit testing environment, and they can be used in regular code instead of full lifetime management wrappers. They do require an extra line of code compared to smart pointer implementations, but they still simplify code and make try...finally blocks redundant.

And here is a full implementation of auto-release managers:

interface type IAutoReleasePool = interface procedure Clear; procedure Add(aInstance: TObject); end; AutoRelease = record public class function New(aInstance: TObject): IInterface; static; class function NewPool: IAutoReleasePool; static; end; implementation type TAutoRelease = class(TInterfacedObject) strict private fInstance: TObject; public constructor Create(aInstance: TObject); destructor Destroy; override; end; TAutoReleasePool = class(TInterfacedObject, IAutoReleasePool) strict private fInstances: TList<TObject>; public constructor Create; destructor Destroy; override; procedure Clear; procedure Add(aInstance: TObject); end; constructor TAutoRelease.Create(aInstance: TObject); begin fInstance := aInstance; end; destructor TAutoRelease.Destroy; begin fInstance.Free; inherited; end; constructor TAutoReleasePool.Create; begin fInstances := TObjectList<TObject>.Create; end; destructor TAutoReleasePool.Destroy; begin fInstances.Free; inherited; end; procedure TAutoReleasePool.Clear; begin fInstances.Clear; end; procedure TAutoReleasePool.Add(aInstance: TObject); begin fInstances.Add(aInstance); end; class function AutoRelease.New(aInstance: TObject): IInterface; begin Result := TAutoRelease.Create(aInstance); end; class function AutoRelease.NewPool: IAutoReleasePool; begin Result := TAutoReleasePool.Create; end;

Comments

  1. Why won't one just use IShared from Spring4D? Admittedly, AutoRelease is slightly faster and comes with less overhead but that will rarely be the deciding factor.

    ReplyDelete
    Replies
    1. Where is fun in that ;)

      Smart pointers can be used, too. But their usage is a bit different and because of that implementations are tad more complex.

      My goal is presenting simplest solution for particular use case. In other words, how to achieve something. If someone is already using some library that has similar solution, even if more complicated, then it is easier to use that library than reinventing the wheel.

      On the other hand introducing library as dependency just to use some simple thing may be an overkill.

      Delete
  2. Guard from JCL
    AutoFree from mORMot

    ReplyDelete

Post a Comment

Popular posts from this blog

Delphi 12.1 & New Quality Portal Released

Coming in Delphi 12: Disabled Floating-Point Exceptions

Assigning result to a function from asynchronous code