We Don't Need No Stinking Garbage Collection


iPhone developers are sometimes called names or excluded from polite circles for not having Garbage Collection. Recently I gave a presentation at the London iPhone Developers Group meeting which covered this matter from a few angles - including some techniques of my own. There was enough interest that I thought I should write it up in a more widely distributed format - where I could also expand on some points I glossed over due to time constraints (it was 20 minute slot).

Rubbish

What's that Smell?

So what's the stink about? Well, if you develop for the iPhone you're going to have to use Objective-C (at least for some of your code - e.g. the UI). Until recently Objective-C has not had any form of Garbage Collection. Memory must be managed manually (although helped by a reference counting system). As of Objective-C 2.0 there is now a GC facility but this is only available on Mac OSX - from Snow Leopard on.

Most languages in common use today have Garbage Collection. There are three stand-out exceptions: Objective-C (on the iPhone), C and C++. As it happens this is precisely the same trio that are sanctified for use on the iPhone (I'm deliberately avoiding the issue of front-end languages/ platforms such as Monotouch - especially while their status with respect to the developer licence remains in doubt)!

So why no GC on the iPhone? Before we look at that I think it's worth a quick review of why anyone would think it was needed in the first place.

Remembering to forget your memory usage

Consider this typical snippet of Objective-C code:

NSString* str = [[NSString alloc] initWithFormat: @"One: %d", 1];

// ...

[str release];

The use of NSString here is not that interesting. What we're looking at is that, because we used alloc to get the string we need to remember to call release when we're done. Note that release doesn't necessarily deallocate the memory for the string there and then. The Objective-C runtime uses a reference counting (or retain counting, in Obj-C parlance) system to track when the last reference is released - at which point dealloc will be called - giving the object a chance to clean up its own resources.

This is hardly rocket science, and already has many advantages over the raw C way of doing things where ownership can be passed around like a hot potato and often it is difficult to know if you should be responsible for freeing or not (yes - conventions exist to mitigate this - but they are not standardised).

But when you start putting stretches of code in the middle, maybe with multiple exit points (unless you're a SESE fanatic - and eschew exceptions) it already starts to add mental overhead. Not much, maybe, especially to an experienced developer - but spread across thousands of instances it adds up. When you're writing code you want to focus as much as possible close to the domain level of abstraction - and these language mechanics issues can detract from that.

As well as the pattern shown in the example above there are other variations to consider. If the object is an instance variable you have to put the release in the dealloc method (or set it to nil via a property if you prefer). If the object is obtained from a static convenience constructor (such as [NSString stringWithFormat:]), or if you send it the autorelease message, then you should not call release yourself - but you do need to be aware the the object will live beyond its last use - which may be significant.

If you are given an object, but not passed ownership, and need to keep hold of it then you will need to send it the retain message (then later remember to call release again).

Whichever case it may be, doing it wrong has consequences. Failing to call release will result in leaks (which may lead to a crash - but much later). Failing to call retain could mean that you are using an object that may have been deallocated. This will likely lead to a crash sooner. If you're really lucky it will crash close enough to the source of the problem to help you find it - unless your users find it first.

Leaks and crashes are serious problems. Even if 99% of your code is correct in its memory management, just one or two such bugs can ruin your user experience (not to mention your app store ratings).

Deodorant

There are some tools that can help. For leaks we now have the LLVM static analyser integrated with XCode. This will work with the compiler to analyse your code paths, looking for misplaced releases or retains. It does a pretty good job and I highly recommend using it regularly. But its not perfect. It misses quite a few cases - especially if the code paths get complex. It can also give false negatives.

At runtime we can also use the Leaks instrument, which will track your references and see when objects are still alive after their last use. I suspect that internally it implements garbage collection, or something like it, to do the reference tracking. Instruments itself has got very good lately - making it much easier to filter out all the noise and see just your bits of the code. Again I highly recommend using this tool. But again it won't catch everything. In particular it will only test the code paths you use while its running.

For over releases, or releasing too early, we can check stack dumps. This may help us to track down the source of a crash - if it's not too far from the suspect code. A better tool, though, is NSZombies - enabled with the NSZombiesEnabled environment variable. With this in operation objects that would normally be dealloc'ed get transmuted into zombie objects instead. These will detect if you send any messages to them and log the fact. There are also a handful of other tools and techniques for tracking down leaks and over releases after the event.

So we can cover up the smell to some extent - but just as with real smells, masking is not the same as removing. We have additional mental overhead distracting you from your task - and extra tools and techniques required to apply after the event.

That's rubbish

So why is Garbage Collection not provided for iOS, given that it's been available on the Mac for about three years?

The short answer is: performance. The slightly longer answer is that GC has an overhead. It's an overhead that you pay every time you run your app. Much of the time the overhead may be invisible, or at least barely noticeable. Sometimes, though, it becomes very noticeable. Common tasks such as scrolling through long lists are famously smooth user experiences on iOS devices. Not so on other platforms, which can tend to be glitchy and jerky.

While bad code is something that can plague any platform (and there are other factors, such as hardware acceleration)- at least it is something you control. Issues caused by GC, though, are outside your immediate control. Depending on the GC implementation you may have access to an API that allows you some degree of control - but this is usually in the form of hints.

Whether you are willing to pay the cost or not, whether you think it's an issue or not, others clearly do have issues with it (and they may be your users). So Apple, as platform provider, has taken the design decision to hold back on providing garbage collection.

This leaves us with managing memory in our own apps. Notice I didn't say "manually managing memory" there. Any good developer - faced with implementing the same patterns of code over and over - even small snippets of code - but especially where missing them is easy and dangerous - find ways to factor out the duplication.

This is a problem that was solved years ago in C++, employing a concept known as RAII.

RAII of light

What is RAII, and how can it help us in Objective-C?

RAII stands for Resource Acquisition Is Initialisation - which is a long and complicated sounding name for a simple but powerful concept. It's also an unfortunate name as it sounds like the focus is on "acquisition" and "initialisation", yet the most interesting part is at the end of the lifetime. To explain what I mean let me cast it in Objective-C terms (if you're familiar with C++ and this concept feel free to skip ahead):

An Objective-C class has one or more initialisers, and a dealloc method. We use these methods to manage our resources (usually pointers to other objects).

In our init method (or designated initialiser) we typically either allocate other objects, or retain objects passed in. Either way we own references to them that we must release later, in our dealloc method.

Of course, we never call dealloc directly - it is called by release once the last retain count has gone.

In C++ the equivalent of init methods are called constructors and the analog to dealloc is the destructor. Where C++ differs is that instead of all objects being reference counted heap objects we have either plain heap objects or value types (which typically live on the stack or, if they are member (instance) variables they are scoped by the lifetime of the object they are a member of).

The nice thing about value types is that they are destroyed immediately, and predictably, at the end of their scope. This feature is so important it has its own name: Deterministic Destruction.

{
    MyClass myObject;

    // ...

} // destructor will be called here

Now because the the destructor is called automatically at the end of the scope - regardless of how it leaves that scope (so it allows for early returns or exceptions), and we can write arbitrary code in the destructor, this gives us a hook. We can use this mechanism to write code that will be be called as the execution leaves a scope. In fact this technique, along with templates and a bit of operator overloading, has been used for years to implement wrapper classes for raw C++ pointers that look and feel like pointers but have some sort of automatic memory management mixed in. These classes are known, collectively, as smart pointers. Reference counting smart pointers are just one possibility but there are many smart pointer traits that can be captured this way - such is the flexibility (and complexity) of C++.

This is all well and good - but we don't have custom value types, templates or operator overloading in Objective-C. So what use has this discussion been?

Objective C++

Apple's officially sanctioned languages for iPhone development are Objective-C, C and C++. But actually there is a fourth language, not listed there because it's a hybrid. That "language" is Objective-C++. I use the word language lightly here because Objective-C++ is a more of a glue language than a language you would use in its own right. Typically it is used to bridge the worlds between pure Objective-C and pure C++ in order to keep them at arms length (pun intended) from each other.

But there is no technical reason that you couldn't write an entire application, top-to-bottom, in Objective-C++. The reason you wouldn't typically do so is that they have very different syntaxes, strengths and weaknesses, and design trade-offs. Mixing the two just doesn't look like code written in a single language. C++ is oil to Objective-C's water (and we'll resist the temptation to point out how big the leaks can get if you mix oil and water!).

We're going carve out a new niche here. We're not going to freely mix the languages without constraint. But we're not going to keep them siloed either. Instead we're going to use some judicious helpers from the C++ side to assist our otherwise pure Objective-C. I don't suggest this lightly. There are certainly some that would take issue with this approach. We'll discuss these trade-offs later.

In the follow-on article I'm going to put this all together and show you my Objective-C++ solution: OCPtr.


Please submit or upvote, here - or follow through to comment on Reddit