Thursday 1 February 2018

Optimizing ARC the hard way?

The problem

In a recent blog post, Dalija Prasnikar talks about how hard it would be to optimize ARC code, because most ARC object references will be passed by value, but not as const. Passing as const would eliminate the need for an implicit __ObjAddRef() call each time the object is passed (and an __ObjRelease() call when the routine with the parameter ends).

OK, ARC (automatic reference counting) has been used for many types already, most of all strings, dynamic arrays and interfaces. Code passing strings and dynarrays is generally optimized already, by specifying such parameters as const. There are probably only a few exceptions. It is also customary to do it with interfaces, and the few exceptions probably don't make a big difference in performance. But it is far from customary to do this for objects. Until ARC, it didn't matter if you passed an object as const or not. But these days, in ARC, it can make a big difference. And objects are passed around a lot, e.g. the Sender parameter of events, etc.

Note that most runtime and FMX code doesn't use const to pass objects either, nor do any of the third parties. Dalija notes that it is not hard to add const to each object parameter, but that it would break huge amounts of code, not only Embarcadero's, but everyone's.

Switch?

I read that and have been thinking about it a little. I first thought of introducing a switch (sound familiar?) in the new ARC compilers, that would make const optional and reduce compiler complaints about interface changes, to make this transition easier. But that would still require a huge amount of code changes, even if it could perhaps be done at a slower pace.

Const by default?

But then I thought of something else. If an object is passed as call-by-value, to a method or function, inside that method or function, you very seldom change the reference by assigning a new object to it. In other words, inside such a method or function, you hardly ever do something like:

    ObjectParam.Free; // necessary in non-ARC, optional in ARC
    ObjectParam := TSomeObject.Create;

Yes, you often change the state of the object by setting properties or by calling methods, but that can be done on a const reference as well. Const means you can't change the reference, not that you can't change the state of the object it refers to.

For almost all practical purposes, such objects passed can be treated as if they were passed as const already. So it would perhaps make sense, in a new version of the ARC compilers, to treat every pass-by-value as const. This would make the code compatible with the non-ARC compilers, while still avoiding a truckload of reference counting calls. This would probably optimize ARC code quite a lot. Of course code already using const would be compatible too. And it would not apply to code using var or out, only to plain pass-by-value object reference parameters.

And the very few methods that do actually re-use a parameter like above should simply be rewritten to use a (new) local variable for the new object. I doubt there is lot of code that does this anyway, so this change would not break a lot of code at all.

So making all passed-by-value objects const by default would probably break very little code, and optimize a lot of ARC code. It would not affect non-ARC code.

As for the old compilers: they would remain non-optimized, but their code would not have to be changed. A simple recompile (and the occasional change) would suffice.

I'd love to hear about your thoughts.

8 comments:

  1. I am puzzled by your statement that IInterface parameters are usually passed as const, but not TObject descendants. Maybe it's just me as I am, compared to many others, still relatively new to Delphi, but I do the exact opposite: If there is no good reason, then objects are passed as const. Interfaces are usually NOT passed as const because stuff like takeInterface( TInterfacedObject.Create() ) creates a memory leak.

    I believe that 1:1 re-usage of code that was written for a different memory management model will never work. For me, it already started with the create..try..finally..destroy-pattern. It's not for ARC compilers. We tried (not succeeded) with one FireMonkey project, and the "truckload of reference counting calls" surely was the least of our problem.

    ReplyDelete
    Replies
    1. There have been many successful Android and iOS projects already, so it beats me why you didn't succeed. The Create-try-finally-Free (not: Destroy -- never call Destroy directly) pattern can be used in both non-ARC and ARC, although in ARC, it is only needed if other than mere memory resources must be protected. Perhaps, some of the times, you had to call DisposeOf. This is all clearly and easily explained in the documentation. So once you get your project going, refcounts *become* a problem.

      Delete
    2. Using conts on object prameters: That may be what you do, but if you look at any piece of code that comes with Delphi, including the runtime etc., you will see that objects are generally *not* passed as const (because until ARC was introduced, there was no need to do that), while strings, dynarrays and interfaces are, for the same reason as this article: eliminating refcounts. So what I wrote may puzzle you, but it reflects reality.

      Delete
    3. FWIW, takeInterface(TInterfacedObject.Create), if takeInterface formally has a const parameter, will NOT create a memory leak. On the contrary: because the refcount is 0, it can cause premature destruction (if the interface is referenced and then released again), resulting in an invalid reference. This is the opposite of a leak. The object doesn't live long enough, instead of too long. This does not happen with ARC-ed objects, AFAIK. And for interfaces, it doesn't happen either, if you write takeInterface(TInterfacedObject.Create as IInterface). Then the refcount is set to 1 before the call.

      Delete
    4. Thank you very much for your elaborate reply. The "T.Create() as IYourInterface" never entered my mind, I will have to check that out. Many thanks!

      I hope this does not get outside the scope of your article, but can you expand on the "create..try..finally..free"? If it entered the try block, then the variable is assigned. There never was a reason to call .Free() and we always used the destructor directly. All our code is like this and I don't see what could be wrong with it (until ARC came along).

      The last thing: Yes, I have been selfish and only thought about our own code. Of course, the Delphi RTL never adds const to reference types. I always disliked that, but the (then, non-present) reference counting was not it. I was worried about copying the reference to the stack every time altough it was totally not needed. Doesn't that apply as well?

      Delete
    5. Especially, if you want to port your code to ARC, use Free and DisposeOf, also on non-ARC platforms. Free checks Self for nil and only calls Destroy if it is not. There are situations where calling Destroy directly will do, but these are pretty rare, so it is best to always use Free, to avoid any problems. That way, there is no need to think about it, because if you must think about it each time, chances are you make mistakes (and it takes developer time, and it is a nuisance). It should become an automatism to use Free. Of course in the times of ARC, it even becomes more important.

      What is this nonsense about "copying a reference to the stack"? Generally, parameters (including object references and other pointers) are passed in registers, and they must be passed anyway, const or not. Note that object references are references already. The only difference is that under ARC, if non-const, refcounting will take place.

      Also, you seem to be confusing const with pass-by-reference. Under const, integers and pointers (including object references) and all other "small" parameters are passed by value. Only types that exceed register size are passed by reference. Please read the documentation about this. Do not assume, but learn.

      Delete
    6. FWIW, http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.TObject.Free: "Use Free to destroy an object. Free automatically calls the destructor if the object reference is not nil. Any object instantiated at run time that does not have an owner should be destroyed by a call to Free, so that it can be properly disposed of and its memory released. Unlike Destroy, Free is successful even if the object is nil; if the object was never initialized, Free would not result in an error."

      Delete
  2. This comment has been removed by the author.

    ReplyDelete