8

I've written some Objective-C application that seems to work fine, except for an ever-growing memory footprint. I'm using ARC under the last version of Xcode 4.6.2. My system is 10.7.5.

I'm very new to Objective-C and need some help figuring out what's going on with my memory. I've narrowed down the problem to explaining why the following basic piece of code behaves like it does.

This code is added in the vanilla Cocoa-based application template that Xcode gives (ARC enabled).

Case A

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification{
    NSDate* d;
    for(int i=0; i<1000000; i++){
        d = [[NSDate alloc] init];
    }
}

Everything goes as expected, ARC reclaims the memory on the fly. Namely, the memory usage history is very flat. When the for loop ends, the app takes about 25MB memory.

Case B

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification{
    NSDate* d;
    for(int i=0; i<1000000; i++){
        d = [NSDate date];
    }
}

Here things are really mysterious to me. When just running the app, the (real) memory usage keeps increasing to about 53MB and then stays there, forever.

However, when running the allocations profiler tool, I can see that at the end of the for loop, all the objects are deallocated, very much like you would expect with an autorelease pool. Also, enclosing the body of the for loop with an @autoreleasepool{} makes case B behave like case A (as expected).

So, three questions:

  1. Under ARC, what is the difference between using the "autoreleased" [NSDate date] and the alloc init object? (I thought there would be close to none from other questions here.)
  2. Why doesn't ARC seem to kick in when running the code?
  3. Why is there a difference in the memory behavior of the profiled application and the actual application?
Mayur Prajapati
  • 5,454
  • 7
  • 41
  • 70
akant
  • 81
  • 3

2 Answers2

4

Autorelease objects are released eventually, whereas in the alloc/init case they are released as soon as they're not used anymore.

This behavior causes your objects to persist in memory for the whole loop in case they are autoreleased for the being released later on, whereas if you alloc/init them, a release method is sent within the loop body.

You can easily make the body loop memory-efficient by wrapping it in a @autoreleasepool like follows:

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification{
    @autoreleasepool {
        NSDate* d;
        for(int i=0; i<1000000; i++){
            d = [NSDate date];
        }
    }
}

This will give a hint to ARC, signaling that you want to create and release an autorelease pool at every loop iteration.

In the specific case the easiest option would probably be to use alloc/init methods, in order for the compiler to do the right thing automatically, but if you have a loop body with many factory methods that return autoreleased instances, the @autoreleasepool block is probably a great way to go.

As a final remark, @autoreleasepool is not an ARC exclusive. It used to exist since LLVM 3.0 it, and with sufficient modern targets (namely iOS5 and OSX 10.7 on) it's known to be much faster than the old-fashioned NSAutoreleasePool.

Gabriele Petronella
  • 106,943
  • 21
  • 217
  • 235
2

[NSDate date] is creating an auto-released object that will be released the next time your program enters the event loop.

The other case is released within the loop by ARC.

If you really want to do something like this you can create your own auto-release pool and drain it periodically. See for example Objective-C: Why is autorelease (@autoreleasepool) still needed with ARC?

Community
  • 1
  • 1
Bryan
  • 11,398
  • 3
  • 53
  • 78
  • Yes, this is indeed what the documentation says. I'm however very unclear with the semantics of the "program enters the event loop". Do I have to conclude the program never enters the event loop again? Also, this does not explain the discrepancy of the behavior when profiling or running. – akant Jun 20 '13 at 08:53
  • 1
    @akant: Profiling uses the "Release" configuration with its optimisation flags. The compiler then also recognizes unnecessary retain/autorelease calls and removes them. That should explain the difference. – Martin R Jun 20 '13 at 08:58
  • 1
    The program needs to enter the event loop to respond to touches, repaint the GUI, etc. Since you say you interacted with the GUI, we know it did this. You say the memory usage stayed high - how were you measuring this? It may have had free heap space but the space was not returned to the kernel so the memory allocated to that process was still high. – Bryan Jun 20 '13 at 10:30
  • @Bryan I just used the 'Real mem' column in the Activity Monitor. To test your theory, I scheduled the loop to occur every 10 seconds via a timer, and although the memory usage stays unnecessary high, it does not grow unbounded. Using `@autoreleasepool` on the other hand immediately returns the memory to the kernel! This is very disturbing :/ – akant Jun 20 '13 at 19:21
  • @MartinR I changed the release flags to match the debug ones: same behavior. From my experiment above, I think the profiler measures "true memory usage" of the application, while the Activity Monitor has less visibility of the process memory usage and just returns whatever the kernel gave to the process. Looks like ARC is not too bothered by giving back the memory to the kernel when it comes to autoreleased objects - probably for performance reasons? – akant Jun 20 '13 at 19:26