Knowing When to Let Go: Better Living Through Memory Management
Pages: 1, 2
Retain/Release/Autorelease
Cocoa has a third method up its memory managing sleeve:
-autorelease. Autoreleasing an object is similar to releasing
it, except that it's got an intrinsic delay; it says "wait a bit before
releasing it." But how long is the delay?
Program flow control in Cocoa is generally handled by means of
NSRunLoop instances, whether you're aware of them or not. If
you're writing a Foundation Tool, or spawning a new thread for use with
NSConnection, you'll be creating them yourself, but if you're
just writing a "simple" application, the magic is in your main
function. Apple's default main function implementation calls a function,
NSApplicationMain, with the arguments that the program was
provided with on launch. This function does a lot of work,
including loading your .nib files, setting up the global NSApplication
instance NSApp, and starting the run loops that manage
application events like mouse-clicks.
In addition to managing input from the user and other sources like
ports, NSRunLoop also works with another class,
NSAutoreleasePool, to implement the delay. Here's a quick
summary of what's happening:
The current run loop receives input of some sort.
It then runs the appropriate code: if the user clicked a button, then the button's target is sent the action message specified in the .nib file, and the right method is run.
That method branches off into several objects and they interact. Eventually, an object is autoreleased.
The autoreleased object's retain count stays the same for the moment, but it is added to the list of objects handled by the current autorelease pool.
Eventually, the action method concludes its work, and control is returned to the run loop.
At this time--and notably prior to looping back to continue execution on input--the autorelease pool is "purged" by being released; that is, all the objects it contains are sent the -release message at this time, which will as normal call -dealloc if it has to.
After that, the return loop will operate on the next piece of input with a fresh autorelease pool ready to be filled.
All of this raises a question: Why and when is this useful? To cover
the why, think about the -description method implemented by
NSObject and (hopefully) overridden to return a useful
description of the receiver in all your classes. This method returns an
object, but it's probably not an instance variable and therefore is not
retained by your object. So what is its state? Should it be up to the
calling code to -release it? Autoreleasing removes the confusion.
With this in mind, here's a guideline for the use of
-autorelease:
If you need to defer ownership of an object, autorelease it.
To simplify that, if you want to create an object and give it to something else and then completely forget about it, make sure that it's autoreleased. Another good example is if you want to create an array without having to release every object you add to it after you're done; autorelease, and it's all done for you.
If you look at the Cocoa classes, there are lots and lots of examples
of methods that autorelease. All of the convenience creator methods--class
methods other than +alloc which return an object--autorelease
as a, you guessed it, convenience.
This has two important ramifications. The first is writing convenience creators for our own classes and is dead simple. Here's an example that's almost a template:
+(id)funkyObjectWithFriend:(id)aFriend
{
return [[[self alloc] initWithFriend:aFriend] autorelease];
}
Like I said, dead simple.
Next, let's think about the example of -description. Should we call -autorelease or is it already called? To answer this, we'll look at another example:
-(id)description
{
return [NSString stringWithFormat:...];
}
In this example, we're calling one of NSString's convenience
creators. So we can be certain that the returned object is already
autoreleased, and that there is no need for us to autorelease it
ourselves. But for a more complex example, what if, for some reason, there
is no convenience creator for exactly what we need? In this case, we'll be
calling +alloc instead of a convenience creator, and so we
will have to autorelease it ourselves. Remember, if you created it
yourself with +alloc, -copy, or
-mutableCopy, then it's up to you to release or autorelease
it! Going the other way, it's safe to assume that if you're getting an
object that something else created, you do not have to autorelease.
Because of the way -autorelease operates, it is not safe
to assume that just because the object you receive exists now, it will
exist until you're done with it. This makes it absolutely vital to
remember that if you need an object that you did not create to stay around
until you're done, you need to retain it. Otherwise, it could easily be
deleted out from under you, causing no end of frustration. Always
remember: if you need it but did not create it, retain it.
One thing that can help you with this is to write -set...
methods for all your ivars, even if you don't make these methods public,
and use them instead of setting the ivars directly. A simple example
that's a good template for single-threaded use (multithreading-safe
accessors are beyond the scope of this document) is as follows:
-setFriend:(id)aFriend
{
id old = friend; // friend is an ivar
friend = [aFriend retain];
[old release];
}
This is basically a retaining swap; out with the old, in with the new.
You might be asking yourself why we don't just use
-autorelease all the time since it makes things so much
easier. Let's take a look.
As you'll recall from our discussion of autoreleasing, the autorelease
pool has to keep a list around of all the autoreleased objects and
-release them all when it's purged at the end of the run
loop. Because of all this "bookkeeping" it has to do, autoreleasing is
inefficient. Many experienced Cocoa programmers will in fact recommend
that you autorelease only when you have to in order to keep complex
programs responsive.
So is there no way to have your cake and eat it too? Is it always a choice between efficiency and ease-of-coding? Let's look at a few of the general issues facing programmers working on memory management systems.
Fragmentation, Efficiency, Concurrency
Three of the biggest problems memory management has to overcome are fragmentation, efficiency, and concurrency. Fragmentation will be familiar to you if you've ever had to "defrag" a hard drive. In short, it's the tendency for storage, be it memory or a drive, to go from an ordered state to an unordered one.
Why is it bad? Fragmentation means that for purposes of memory management, you have to keep large, slow lists of memory blocks around, just like the autorelease pool does. In a perfect world, objects would arrange themselves nicely so you could just get rid of a whole group of them at once by clearing a single, large, specific section of memory. Unfortunately, our world is not so perfect, and repeated allocations and deallocations compounded by the realities of virtual memory can lead us right into the next point: a lack of efficiency.
Memory operations, whether they're allocations or deallocations, are among the most commonly run operations in all but the simplest programs. Even simple matters like adding an object to an array ends up involving several allocations; the object and array both have to be allocated, and then the array has to add the space required for the object you add. And because modern operating systems feature memory protection to keep them from crashing as well as virtual memory, there's overhead inherent to all of these operations.
In complex programs, it can be beneficial to write code to grab a single large section of memory from the system all at once, and then do smaller allocations from it yourself; this effectively means you're doing it all yourself, however, and as such is not for the faint of heart. And doing it yourself leads us heavily into our third issue: concurrency.
Along with memory protection and virtual memory, modern operating systems allow and even encourage the use of multitasking and multithreading. The first is the capacity for sharing the system's resources between multiple programs such that they're run alongside one another, concurrently. This isn't much of a problem for memory management unless you happen to be a systems programmer, but that is quite out of the scope of this article.
The second is the capacity for multiple parts of a single program to be run at the same time, and this is a much more pertinent problem to the average Cocoa developer. Multithreading is a complex issue, but the problems it presents for memory management are much like the problems it presents for anything else, except possibly worse: if two sections of code can be run at once, they can be trying to access the same memory at once, and can calmly and quietly run amok all over each other, causing your program to fail quite spectacularly. So the programmer responsible for the memory management code has to think about what operations should be made thread-safe and at what cost, because all thread safety measures cause a reduction in direct efficiency.
To answer the question, no, you can't have your cake and eat it too. There are many more balances to be found between efficiency and ease than the one provided by Cocoa's retain/release/autorelease system, but they are perhaps better the subject for a future article. If you're interested, however, I recommend that you look at MemoryManagement.org and browse the glossary for terms such as "conservative garbage collection" and "reference counting."
It's clear that memory management is a complex issue, but hopefully this article has provided you both with a handle on using it in Cocoa and an idea of what's at work (or play) behind the scenes. I hope you've enjoyed this as much as I have!
Rob Rix is a renaissance man masquerading as a specialist, and is Canadian to boot.
Return to Mac DevCenter.
You must be logged in to the O'Reilly Network to post a talkback.
Showing messages 1 through 14 of 14.
-
hmmm
2003-06-20 19:36:37 anonymous2 [Reply | View]
-
hmmm
2003-06-23 09:03:21 anonymous2 [Reply | View]
You are of course completely correct, and it's nice to have your comments on it, but it still violates the DoTheSimplestThingThatCouldPossiblyWork philosophy (as featured on the Portland Pattern Repository).
For simple programs, autoreleasing in this sort of situation is not just convenient but probably preferable since you don't need the gain in efficiency.
I suppose I had better restate the ideal:
In programs where efficiency is key, avoid autoreleasing if possible. In programs where simplicity is key, autorelease where the convenience helps.
Your mileage may vary; strike your own balance.
-
NSLog blog entry on this subject
2003-06-19 07:27:59 anonymous2 [Reply | View]
Erik has a blog entry about this at NSLog(). It goes into a bit more detail (including when you might like to [object copy] rather than [object retain]!
http://nslog.com/archives/2003/05/17/accessor_methods_part_2.php
-
is simpler slower?
2003-06-12 21:54:15 anonymous2 [Reply | View]
The following algorithm is used in the The Objective-C Programming Language document from the Apple developer's site. Although the code is a little simpler then creating a temporary variable for nothing more than a placeholder for an object you're going to release, I'm not sure that it gives you a net gain in efficiency. Does the extra overhead from the "autorease" outway the penalty involved in creating an extra variable?
-setFriend:(id)aFriend
{
[friend autorelease]; // friend is an ivar
friend = [aFriend retain];
return;
}
-
is simpler slower?
2003-06-12 22:07:15 iapole [Reply | View]
Completely subjectively--yes. In the example accessor method I gave (which is not the only possible one, as I'll show in a moment), the penalties are as follows:
- one 4-byte word for the pointer (id old)
- two messages, both of which will have been optimized by Apple considering how commonly they're called
- one assignment.
In the example you show here, you have the same except for the pointer to the object, but the difference in message (autorelease as opposed to release) will make a much larger difference.
For one thing, in order to autorelease the object, the autorelease pool has to know it exists--that is, it has to use a pointer to it of its own. And as I discussed in the article, autoreleasing is a slow operation.
Therefore, I would recommend against autoreleasing in this context unless you have some very specific reason to.
As another example of an accessor, how about:
-(void)setFriend:(id)aFriend
{
[friend release];
friend = [aFriend retain];
}
Note that none of these examples make any attempt at addressing concurrency.
I recommend reading http://www.cocoadev.com/index.pl?AccessorMethods for more insight into Cocoa accessor methods, including thread-safe accessors. -
is simpler slower?
2003-06-20 18:05:46 anonymous2 [Reply | View]
"I recommend reading http://www.cocoadev.com/index.pl?AccessorMethods for more insight into Cocoa accessor methods, including thread-safe accessors."
The more complete article it references is at:
http://www.stepwise.com/Articles/Technical/2002-06-11.01.html/
That in turn references another more general introduction to memory management
...
mmalc -
is simpler slower?
2003-06-13 14:27:00 anonymous2 [Reply | View]
one thing to worry about when using the accessor method you suggested in your post is the situation where the same object passed to the method was already the one set! calling retain on aFriend could then potentially result in a call to a nonexistent object.
-
is simpler slower?
2003-06-13 20:52:04 iapole [Reply | View]
There, I knew there was a good reason not to use it! Thanks, I just couldn't remember it for the life of me. -
is simpler slower?
2003-06-15 21:29:50 anonymous2 [Reply | View]
Perhaps this would be faster?
-setFriend:(id)aFriend
{
if (aFriend != friend)
{
[friend release];
friend = [aFriend retain];
}
}
I suppose it depends on the efficency of the comparison operation. -
is simpler slower?
2003-06-16 16:11:16 iapole [Reply | View]
I believe the PPC's branch prediction is good enough to make that fast, but faster still will probably be:
-(void)setFriend:(id)aFriend
{
[aFriend retain];
[friend release];
friend = aFriend;
}
Which avoids the releasing problem. But keep in mind that this is all subjective, I have done absolutely no tests of efficiency regarding these.
-
What about preallocated memory?
2003-06-11 16:10:11 anonymous2 [Reply | View]
Many apps tend to exhaust the CPU with simple memory allocations.
Especially apps that allocate and free memory dynamically upon events (say,an incoming network packet).
Working with preallocated memory and reusing this memory without going through the system calls free and malloc often enhances the performance of these apps dramatically.
It's a bit more complex, but it's worth your while if you have this kind of app. -
What about preallocated memory?
2003-06-11 16:59:28 anonymous2 [Reply | View]
However it is also important to realise that malloc and free are not system calls. This is important because while brk/anon-mmap are relatively expensive, malloc and free are signifigantly cheaper. In most cases you are better off tuning your allocator then avoiding dynamic allocation.
There are problems that require pre-allocation, but they are few, and far between. -
Re: What about preallocated memory?
2003-06-11 22:01:17 iapole [Reply | View]
Significantly cheaper, yes, but still expensive from some perspectives. A lot of my recent coding has been done with OpenGL, and I anticipate a point in the future where allocations will be the major bottleneck. It may be farther off than I think, of course, and I'll wait till then to optimize... but it's still a fun thing to know how to do! -
Re: What about preallocated memory?
2003-06-11 16:26:16 iapole [Reply | View]
Agreed, definitely. Because you're working within a simpler domain than e.g. the whole system, you don't have to worry about all the stuff that the malloc/free developers had to, so you can avoid some overhead.
Of course, there's no real substitute for good design in this sort of matter. You won't get as many benefits from preallocation if your preallocated memory has to be accessible concurrently by several threads, for instance, so planning ahead is (as always) advisable.






"With this in mind, here's a guideline for the use of -autorelease:
* If you need to defer ownership of an object, autorelease it."
I would suggest that that be re-written as "If you need to defer relinquishment of ownership..." If you have retained an object, you already "own" it; you want to give up ownership at some later time.
In the next paragraph:
"To simplify that, if you want to create an object and give it to something else and then completely forget about it, make sure that it's autoreleased. Another good example is if you want to create an array without having to release every object you add to it after you're done; autorelease, and it's all done for you."
This is a misleading over-simplification, and contradicts your later (itself perhaps over-exaggerated) exhortation to avoid autorelease.
It is perfectly reasonable to simply release an object that has been put into an array. Contrast
aNumber = [NSNumber numberWithInt:n];
[aMutableArray addObject:aNumber];
with
aNumber = [[NSNumber alloc] initWithInt:n];
[aMutableArray addObject:aNumber];
[aNumber release];
The latter is both correct from the perspective of memory management and more efficient.
mmalc