Give in to the ARC side

Posted by on in Blogs
I thought I’d take a few moments (or more) to answer some questions regarding the just introduced Automatic Reference Counting (ARC) mechanism to Delphi on mobile platforms (namely iOS in XE4, and Android in a future release). Among some sectors of our customer base there seems to be some long-running discussions and kvetching about this change. I will also say that among many other sectors of our customer base, it’s been greeted with marked enthusiasm. Let’s start with some history…

Meet the new boss, same as the old boss.


For our long-time Delphi users, ARC is really nothing new. Beginning back in Delphi 2 (early 1996), the first Delphi release targeting Windows 32bit, the Delphi compiler has done ARC. Long-Strings, which broke free from the long standing Turbo Pascal limit of 255 characters, introduced the Delphi developer to the whole concept of ARC. Until then, strings (using the semi-reserved word “string”) had been declared and allocated “in-place” and were limited to a maximum of 255 characters. You could declare a string type with less than 255 characters by using the “string[<1-255>]” type syntax.

Strings changed from being allocated at compile time to a reference type that pointed to the actual string data in a heap-allocated structure. There are plenty of resources that explain how this works. For the purposes here, the heap-allocated data structure could be shared among many string variables by using a “reference-count” stored within that data structure. This indicated how many variables are pointing to that data. The compiler managed this by calling special RTL helper functions when variables are assigned or leave the current scope. Once the last string reference was removed, the actual heap-allocated structure could be returned to the free memory pool.

Hey this model works pretty well… Oh look! MS’ COM/OLE/ActiveX uses ARC too!


Delphi 3 was a seminal release in the pantheon of Delphi releases. For those who remember history, this is the first Delphi release in which the founder of Turbo Pascal, and the key architect of Delphi, Anders Hejlsberg, was no longer with the company. However, even with the departure of Anders, Delphi 3 saw the release of many new features. Packages and interfaces were major new language enhancements. Specifically interesting were interfaces, which placed support for COM/OLE/ActiveX-Style interfaces directly into the language.

The Delphi compiler already had support and logic for proper handling of an ARC type, strings, so it was an incremental step forward to add such support for interfaces. By adding interface ARC directly into the language, the Delphi developer was freed from the mundane, error-prone task of manually handling the reference counting for COM interfaces. The Delphi developer could focus on the actual business of using and accessing interfaces and COM/ActiveX objects without dealing with all those AddRef/Release calls. Eventually those poor C++ COM developers got a little help through the introduction of “smart pointers”… But for a while, the Delphi developers were able to quietly snicker at those folks working with COM in C++… oh ok, we still do…

Hey, let’s invite dynamic arrays to this party!


Introduced in Delphi 4, dynamic arrays freed developers from having to work with non-range-checked unbounded arrays or deal with the hassles of raw pointers. Sure there are those who get a clear kick out using raw memory locations and mud-wrestling and hog-tying some pointers… Dynamic arrays took a few pages from the string type playbook and applied it an array data type whose elements are now user-defined. Like strings, they too use the ARC model of memory management. This allows the developers to simply set their length and pass them around without worrying about who should manage their memory.

If ARC is good, Garbage Collection is sooo much better…(that’s a joke, son)


For a while, Delphi cruised along pretty well. There was Delphi 5, then a foray into the hot commodity of the time, Linux where the non-Delphi-named Kylix was developed. Delphi 6 unified the whole Windows and Linux experience… however not much really happened on the compiler front other than retargeting the x86 Delphi compiler to generate code fitting for Linux. By the time Delphi 7 was released, work on the Delphi compiler had shifted to targeting .NET (notice I have ignored that whole “oh look! didn’t Borland demonstrate a Delphi compiler targeting the Java VM?” thing… well… um… never-mind…). .NET was the culmination (up to that time) of the efforts of Anders Hejlsberg since he’d left for Microsoft several years prior.

Yes there has been a  lot of discussion over the years about whether or not MS really said that .NET was the “future” of the Windows API… From my recollection, I precisely remember that being explicitly stated from several sectors of the whole MS machine. Given the information at the time, it was clearly prudent for us to look into embracing the “brave new world” of .NET, it’s VM, and whole notion of garbage collection. We’re not mind readers, so regardless of any skepticism about this direction (oh and there was plenty of that), we needed to do something.

Since I have the benefit of 20/20 hindsight and having been a first-hand witness and participant in the whole move to .NET, there are some observations and lessons to be learned from that experience. I remember the whole foray into .NET as generating the most number of new and interesting language innovations and enhancements than I’ve seen since Delphi 2. A new compiler backend generating CIL (aka. MSIL), the introduction of class helpers which allow the injection of methods into the scope of an existing class (this was before C# “extension methods” which are very similar, if not more limited than helpers). I also think there were some missteps, which at the time, I know I vehemently defended. Honestly, I would probably have quite the heated discussion if the “me” of now were to ever meet the “me” of then Winking smile.

Rather than better embrace the .NET platform, we chose to mold that platform into Delphi’s image. This was reinforced by the clearly obvious link between the genesis behind Delphi itself and the genesis of .NET. They both were driven by the same key developer/architect. On several levels this only muddled things up. Rather than embracing GC (I have nothing really against GC, in fact, I find that it enables a lot of really interesting programming models), we chose to, more or less, hide it. This is where the me of now gets to ridicule the me of then.

Let’s look at one specific thing I feel we “got wrong”… Mapping the “Free” method to call IDisposable.Dispose. Yes, at the time this made sense, and I remember saying how great it was because though the magic of class helpers you can call “Free” on any .NET object from Delphi and it would “do the right thing.” Yes, of course it “did the right thing”, at the expense of holding the platform at arm’s length and never really embracing it. Cool your jets… Here, I’m using the term “platform” to refer more to the programming model, and not the built-in frameworks. Not the .NET platform as a whole…People still wrote (including us) their code as if that magic call to Free was still doing what it always has done.

The Free method was introduced onto TObject for the sole purpose of exception safety. It was intended to be used within object destructors. From a destructor, you never directly called Destroy on the instance reference, you would call Free which would check if the instance reference was non-nil and then call Destroy. This was done to simplify the component developer’s (and those that write their own class types) efforts by freeing (no pun intended) them from doing that nil check everywhere. We were very successful in driving home the whole create...try…finally…free coding pattern, which was done because of exceptions. However, that pattern really doesn’t need to use Free, it could have directly called Destroy.
Foo := TMyObj.Create;
try
{... work with Foo here}
finally
Foo.Free; {--- Foo.Destroy is just as valid here}
end;

The reason that Foo.Destroy is valid in this instance is because if an exception were raised during the create of the object, it would never enter the try..finally block. We know that if it enters the try..finally block the assignment to Foo has happened and Foo isn’t nil and is now referencing a valid instance (even if it were garbage or non-nil previously).

Under .NET, Free did no such thing as “free” memory… it may have caused the object to “dispose” of some resources because the object implemented the IDisposable interface pattern. We even went so far as to literally translate the destructor of declared classes in Delphi for .NET into the IDisposable pattern, even if all that destructor did was to call free other object instances, and did nothing with any other non-memory resource. IOW, under a GC environment it did a whole lot of nothing. This may sound like heresy to some, but this was a case where the power of the platform was sacrificed at the altar of full compatibility.

Come to the ARC side


What is different now? With XE4, we’ve introduced a Delphi compiler that directly targets the iOS platform and it’s ARM derivative processor. Along with this, ARC has now come to full fruition and is the default manner in which the lifetime of all object instances are managed. This means that all object references are tracked and accounted for. This also means that, like strings, once the number of references drop to 0, the object is fully cleaned up, destroyed and it’s memory is returned to the heap. You can read about this in detail in this whitepaper here.

If you ask many of my coworkers here, they’ll tell you that I will often say, “Names are important.” Names convey not only what something is, but in many cases what it does. In programming this is of paramount importance. The names you choose need to be concise, but also descriptive. Internal jargon shouldn’t be used, nor should some obscure abbreviation be used.

Since the focus of this piece (aside from the stroll down memory lane) is surrounding “Free”, let’s look at it. In this instance, Free is a verb. Free has become, unfortunately, synonymous with Destroy. However that’s not really it’s intent. It was, as stated above, was about writing exception-safe destructors of classes. Writing exception-safe code is also another topic deserving of its own treatment.

Rather than repeat the mistake in Delphi for .NET in how “Free” was handled, Free was simply removed. Yes, you read that right. Free has been removed. “What?” But my code compiles when I call Free! I see it in the RTL source! We know that there is a lot code out there that uses the proverbial create..try..finally..free pattern, so what is the deal!? When considering what to do here, we saw a couple of options. One was to break a lot of code out there and force folks to IFDEF their code, the other was to find a way to make that common pattern still do something reasonable. Let’s analyze that commonly implemented pattern using the example I showed above.

Foo is typically a local variable. The intent of the code above is to ensure that the Foo instance is properly cleaned up. Under ARC, we know that the compiler will ensure that will happen regardless. So what does Foo.Free actually do!? Rather than emit the well known “Unknown identifier” error message, the compiler simply generates code similar to what it will be automatically generated to clean up that instance. In simplistic terms, the Foo.Free; statement is translated to Foo := nil; which is then, later in the compile process, translated to an RTL call to drop the reference. This is, effectively, the same code the compiler will generate in the surrounding function’s epilogue. All this code has done is simply do what was going to happen anyway, just a little earlier. As long as no other references to Foo are taken (typically there isn’t even in Non-ARC code), the Foo.Free line will do exactly what the developer expects!

“But, but, but! wait! My code intended to Free, destroy, deallocate, etc… that instance! Now it’s not!” Are you sure? In all the code I’ve analyzed, which includes a lot of internal code, external open-source, even some massive customer projects used for testing and bug tracking, this pattern is not only common, it’s nearly ubiquitous. On that front we’ve succeed in “getting the word out” about the need for exception safety. If the transient local instance reference is the only reference, then ARC dictates that once this one and only one reference is gone, the object will be destroyed and de-allocated as expected. Semantically, your code remains functioning in the same manner as before.

To steal a line from The Matrix, “You have to realize the truth… there is no Free”.

Wait… but my class relies on the destructor running at that point!


Remember the discussion above about our handling of IDisposable under Delphi for .NET? We considered doing something similar… implement some well-known interface, place your disposal code in a Dispose method and then query for the interface and call if present. Yuck! That’s a lot of work to, essentially, duplicate what many folks already have in their destructors. What if you could force the execution of the destructor without actually returning the instance memory to the heap? Any reference to the instance would remain valid, but will be referencing a “disposed” instance (I coined the term a “zombie” instance… it’s essentially dead, but is still shambling around the heap). This is, essentially, the same model as the IDisposable pattern above, but you get it for “free” because you implemented a destructor. For ARC, a new method on TObject was introduced, called DisposeOf.

Why DisposeOf, and not simply Dispose? Well, we wanted to use the term Dispose, however, because Dispose is also a standard function there are some scoping conflicts with existing code. For instance, if you had a destructor that called the Dispose standard function on a typed pointer to release some memory allocated using the New() standard function, it would fail to compile because the “Dispose” method on TObject would be “closer in scope” than the globally scoped Dispose. Bummer… So DisposeOf it is.

We’ve found, in practice, and after looking at a lot of code (we do have many million lines of Delphi code at our disposal, and most of it isn’t ours), it became clear that the more deterministic nature of ARC vs. pure GC, the need to actively dispose of an instance on-demand was a mere fraction of the cases. In the vast majority (likely >90%) can simply let the system work. Especially in legacy code where the above discussed create..try..finally..free pattern is used. The need for calling DisposeOf explicitly is more the exception than the rule.

So what else does DisoseOf solve? It is very common among various Delphi frameworks (VCL and FireMonkey included), to place active notification or list management code within the constructor and destructor of a class. The Owner/Owned model of TComponent is a key example of such a design. In this case, the existing component framework design relies on many activities other than simple “resource management” to happen in the destructor.

TComponent.Notification() is a key example of such a thing. In this case, the proper way to “dispose” a component, is to use DisposeOf. A TComponent derivative isn’t usually a transient instance, rather it is a longer-lived object which is also surrounded by a whole system of other component instances that make up things such as forms, frames and datamodules. In this instance, use DisposeOf is appropriate.

For class instances that are used transiently, there is no need for any explicit management of the instance. The ARC system will handle it. Even if you have legacy code using the create..try..finally..free pattern, in the vast majority of cases you can leave that pattern in place and the code will continue to function as expected. If you wanted to write more ARC-aware code, you could remove the try..finally altogether and rely on the function epilogue to manage that instance.

Feel the power of the ARC side.


“Ok, so what is the point? So you removed Free, added DisposeOf… big deal. My code was working just fine so why not just keep the status quo?” Fair question. There is a multi-layered answer to that. One part of the answer involves the continuing evolution of the language. As new and more modern programming styles and techniques are introduced, there should be no reason the Delphi language cannot adopt such things as well. In many cases, these new techniques rely on the existence of some sort of automatic resource/memory management of class instances.

One such feature is operator overloading. By allowing operators on instances, the an expression may end up creating several “temporary” instances that are referenced by “temporary” compiler-created variables inaccessible to the developer. These “temporary” variable must be managed so that instances aren’t improperly “orphaned” causing a memory leak. Relying on the same management mechanism that all other instances use, keeps the compiler, the language, and the runtime clean and consistent.

As the language continues to evolve, we now have a more firm basis on which to build even more interesting functionality. Some things under consideration are enhancements such as fully “rooting” the type system. Under such a type system, all types are, effectively, objects which descend from a single root class. Some of this work is already under way, and some of the syntactical usages are even available today. The addition of “helper types” for non-structured types is intended to give the “feeling” of a rooted type system, where expressions such as “42.ToString();” is valid. When a fully rooted type system is introduced, such an expression will continue to work as expected, however, there will now be, effectively, a real class representing the “42” integer type. Fully rooting the type system will enable many other things that may not be obvious, such as making generics, and type constraints even more powerful.

Other possibilities include adding more “functional programming” or “declarative programming” elements to the language. LINQ is a prime example of functional and declarative programming elements added to an imperative language.

Another very common thing to do with a rooted type system is to actively move simple intrinsic type values to the heap using a concept of “boxing”. This entails actually allocating a “wrapper” instance, which happens to actually be the object that represents the intrinsic type. The “value” is assigned to this wrapper and now you can pass this reference around as any old object (usually as the “root” class, think TObject here). This allows any thing that can reference an object to also reference a simple type. “Unboxing” is the process by which this is reversed and the previously “boxed” value is extracted from the heap-allocated instance.

What ARC would enable here is that you can still interact with the “boxed” value as if it were still a mere value without worrying about who is managing the life-cycle of the instance on the heap. Once the value is “unboxed” and all references to the containing heap-instance are released, the memory is then returned to the heap for reuse.

The key thing to remember here: this is all possible with a natively compiled language without the need for any kind of virtual machine or runtime compilation. It is still strongly and statically typed. In short, the power and advantages gained by “giving in to the ARC side” will become more apparent as we continue to work on the Delphi language… and I’ve not even mentioned some of the things we’re working on for C++…


About
Gold User, Rank: 83, Points: 11

Comments

  • Guest
    David M Tuesday, 18 June 2013

    Allen, I'm not sure I understand your comment: "the best practice for doing large numbers of parallel tasks on sets of data is to ensure that all the memory needed is acquired upfront. Then dispatch different portions of that data to the various parallelized tasks. The more isolation among tasks, the far better your performance is going to be"

    I don't disagree that the more isolated tasks are, the better. But why allocate all memory up-front? A task will do its work and it's perfectly valid for a task to, at some point, allocate the memory it needs. In the context your other comments about the memory manager global lock, it seems this might be a workaround of a contended resource (the lock), not a contended resource (memory in general.)

    /If/ allocating memory in different threads has little contention, what reason is there to allocate all memory that tasks need is acquired upfront?

  • Guest
    Allen Bauer Wednesday, 19 June 2013

    Because memory is a global resource, not only from a process perspective but also from an OS-level perspective. The more you exercise the global memory manager the more chance for contention. Even if you have a new thread aware memory manager that minimized contention, chances are that memory manager will reduce it's contention by grabbing larger blocks of memory from the OS at once, then hand out that memory to each thread independently.

    My point is that whether your code pre-allocates the memory or the memory manager does it, it's likely to happen at some level in order to reduce contention, even if that is happening down at the OS level. You can rely on the memory manager to be efficient or you can work in your own code to, which has a better picture of what the memory footprint and access patterns are likely to be, ensure you're not beholden to whatever the memory manager is doing.

    There are also locality of reference issues that can damage performance. Unless you can be assured that each thread requesting memory from the memory manager is handing out memory from different regions for each thread, you run the risk of two or more threads sharing memory on the same cache-line... of course a simple "fix" for that is to make sure all allocations are aligned on a cache-line boundary. For lots of tiny allocations, that can be *less* efficient use of memory and might increase your memory pressure. Cache-line sized can range from 32 to 256 bytes, depending on the hardware and the CPU involved.

  • Guest
    Craig Peterson Wednesday, 19 June 2013

    Allen,

    Thanks for finally chiming in, though I really wish you (EMBT) had anticipated these reactions and done so much earlier.

    I still don't understand (accept?) the decision to not map Free to DisposeOf. You said that doing so would be "sacrificing" the platform for compatibility, but that's a *good* thing. The 90% figure you mentioned is irrelevant, because most try..finally blocks would work either way. It's the remaining 10% that you're messing up, and that really only in existing code. For any new code that wants to fully use ARC, you wouldn't use either Free or Destroy in most circumstances.

    It really stinks of forcing the new coding model on us in places where it's unwelcome when the alternative would have worked better. I respect your experience and hindsight in the matter, but I don't see anything to the argument besides it being "impure".

  • Guest
    Günther Schoch Wednesday, 19 June 2013

    re: Allen and Bacon.

    >As I’ve already stated, this is certainly something I want to continue to look into…

    Given the amount of code that should be transferred to ARC (3rd party and projects) I would really see a large benefit to improve whatever is possible in that area ASAP.

    Just make the transfer as easy and stable as possible and more and more developer will switch (as well in win32/64).

  • Guest
    Luigi Sandon Thursday, 20 June 2013

    What about ARC and BASM? Will the compiler be able to spot reference changes in ASM code, especially when references are held in registers? Or has it to be managed manually?

  • Guest
    Vladimir Ulchenko Thursday, 20 June 2013

    @David M: shall your new MM beat TBBMM and NexusMM?

  • Guest
    Vladimir Ulchenko Thursday, 20 June 2013

    > and I’ve not even mentioned some of the things we’re working on for C++…

    looking forward to more horror stories on bcb front

  • Guest
    Dimitrij Kowalski Wednesday, 26 June 2013

    @Vladimir Ulchenko lol! ;)

  • Guest
    Wenjie Zhouw Sunday, 30 June 2013

    I have only one opinion:
    Give vs a chance to choose ARC or NOT ARC, just like the relationship between the Object and Class key word.

    If we have no chooice, we will have the feeling like been
    raped. No matter you give us what much wonderfull things.

  • Guest
    Wenjie Zhouw Thursday, 4 July 2013

    By the way. Most things have there advantages and disadvantages. Why not let both of them be there.
    And let users determine how to use them.

  • Guest
    Allen Bauer Friday, 5 July 2013

    So how would a library that is intended to be used with ARC work with other libraries that are not? Let's follow that logic in a thought-experiment...

    Option 1: Compiler switch.

    How would Unit A built with ARC off properly handle an object from Unit B built with ARC on? Wouldn't that really mess up the lifetime and reference counts of that object? What if you don't have the source to either or one of the units? (sorry, merely saying that you don't buy components without source doesn't solve the issue because many customers *do* buy components without source). Let's go even further and assume that each object carries some "flag" indicating that it is ARC enabled... Now *all* code must test this flag and decide whether the particular instance is ARC-enabled.

    Option 2: ARC enabled base class

    How is this a choice? Suppose Unit A's classes are built with the ARC base class and you want them to interact with Unit B's functionality which expects objects descending from the non-ARC base. Since they descend from separate bases, they're not going to be type-compatible.
    At what level in the hierarchy should ARC be enabled? Should there now be two TObject-like base classes which share no common base? If they do share a common base, is that base ARC-on or ARC-off?

  • Guest
    Wenjie Zhouw Sunday, 7 July 2013

    I mean NO ARC and ARC exist in the same time.
    Compiler switch can not achieve this goal .

    We can regard Class as NO ARC and regard Interface as ARC.
    Just like string(ARC) and pchar(NO ARC). That's very good .

    I can choose by myself acording to the different requirements.

    You had mentioned string is ARC.
    OK, it is ARC. But pchar worked well with string and
    they are not exclusive in the old days.

    Why we have to choose one and kill another today?

  • Guest
    Allen Bauer Monday, 8 July 2013

    Ok, I see... you want the status quo. That would mean that things like operator overloads on classes would not be possible.

  • Guest
    Wenjie Zhouw Monday, 8 July 2013

    Can we think about operator overloads in c++.

    Class object in stack and Copy Constructor?

  • Guest
    Wenjie Zhouw Monday, 8 July 2013

    Class object in stack will have more efficiency.

  • Guest

    [...] and unboxed as needed to store it inside a TObject.  However, Delphi is not a rooted language, at least not yet.   Well, the technique is so useful I was unwilling to give [...]

  • Guest
    Wenjie Zhouw Thursday, 1 August 2013

    We all knows that c++ has operator overloads .
    C++ has CComPtr.
    ARC and operator overloads not exclusive .

    I'm very confused for how are you positioning Delphi now.
    Language C is no less because of the fewer language features.
    Language C++ is no less because of the complex language features.
    What's the matter?

    If Delphi is still focus on the Native code. Delphi should
    learn c/c++ more, but not a dynamic language.

    ARC is wonderful. But ARC is not the whole world. Just like class. Now, we can write functions in Record type.
    This also proved that class is not the whole world. The
    world is diverse, we should have a variety of ways to describe the world.
    If you still think ARC is the only one chooice.
    I beg you let record support virtual functions and let record can override.

    Thanks.

  • Guest

    [...] Sender parameter. Yes, the new Android compiler is using ARC, just like the iOS compiler. Check out Allen’s blog post on ARC for a lot more detail on how it [...]

  • Guest
    XantheFIN Friday, 23 August 2013

    Sorry to post here but i got lost and didn't yet find other way but you had some information about Ford J3 diy adapter what you did do? Please contact via email.

  • Guest
    Sqiar BI Friday, 17 January 2014

    data analysis reporting services
    SQIAR (http://www.sqiar.com/solutions/technology/tableau) is a leading Business Intelligence company.Sqiar Consultants Provide Tableau Software Consultancy To small and Medium size of organization.

  • Please login first in order for you to submit comments

Check out more tips and tricks in this development video: