objective-cfoundationobjective-c-runtimensautoreleasepool

Using NSAutoreleasePool only via Objective-C runtime functions


I'm learning how memory management works in Objective-C. From what I've learned, objects that are marked autorelease will be added to the enclosing NSAutoreleasePool and be released whenever the pool is released/drained.

To try out my new knowledge I created some tests; Creating 1 million objects used ~17mb, creating the same objects but immediately releasing them used ~1mb. However, I cannot get NSAutoreleasePool to work, using the code below still uses ~17mb and I do not understand why.

#include <objc/objc-runtime.h>
#include <stdio.h>

int main(void) {
    Class NSAutoreleasePool = objc_getClass("NSAutoreleasePool");
    Class Object = objc_getClass("NSObject");
    SEL new = sel_registerName("new");
    SEL drain = sel_registerName("drain");
    SEL autorelease = sel_registerName("autorelease");
    id pool = objc_msgSend(NSAutoreleasePool, new);
    for (int i = 0; i < 1e6; i++) {
        id obj = objc_msgSend(Object, new);
        objc_msgSend(obj, autorelease);
    }
    objc_msgSend(pool, drain);
    printf("OK\n");
    getchar();
    return 0;
}

Solution

  • When you release each object immediately after creating it, there's never more than one object in existence at a time. So the runtime can recycle the same memory for each new object. The runtime only has to ask the operating system for enough memory to hold that one object.

    When you autorelease each object after creating it, each object lives until the autorelease pool is drained, so you end up with 1,000,000 objects existing simultaneously. The runtime has to ask the operating system for enough memory to hold all of the objects. And since the runtime doesn't know in advance how many objects you intend to create, it asks the operating system for chunks of memory incrementally. Maybe it asks for 1 MiB, and when that 1 MiB is exhausted, it asks for another 1 MiB, and so on.

    Generally, when your program frees some memory (in this case because it destroys an object), the runtime doesn't return that memory to the operating system. It keeps the memory available so that it can reuse it the next time you create an object.

    The operating system knows how much memory it has given to your process, but it doesn't know which parts of that memory are (within the process) consider “in use” and which parts are considered “free”.

    Activity Monitor and top both ask the operating system for information about your process. So they only know what the operating system knows, which is how much memory has been given to your process. They can't know how much of that memory is in use and how much is free.

    If you want a more accurate picture of how much memory is in use by live objects in your program, you need to use a more invasive tool. The Instruments program, which comes with Xcode, can show you how much memory is in use by “live allocations” using the Allocations instrument.

    Here's what the Allocations instrument shows, when I run your program under it for about three seconds:

    Allocations instrument

    So you can see that the Allocations instrument detected a total of 1,001,983 “transient” allocations. A transient allocation is a chunk of memory that was allocated and then freed before Instruments stopped recording.

    You can also see that the “persistent” bytes allocated (which is memory allocated and not freed by the time Instruments stopped recording) is 506 KiB, but the total bytes allocated (including bytes later freed) is 23.5 MiB.