2

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;
}
jscs
  • 63,694
  • 13
  • 151
  • 195
user1768788
  • 1,265
  • 1
  • 10
  • 29
  • Is there a reason you are not using any Objective-C syntax for this? Do you actually want pure C code? If so, see https://stackoverflow.com/questions/10289890/how-to-write-ios-app-purely-in-c for related info. – rmaddy Jan 29 '19 at 17:33
  • @rmaddy Not really. I can get this example to work in plain objective-c but I'm curious why it does not work when using just the runtime and compiling from regular C. Am I doing something wrong or is this behavior expected? Also the question you linked does not address this issue. The accepted answer also claims that `NSAutoreleasePool` is deprecated which the apple documentation does not say. – user1768788 Jan 29 '19 at 17:48
  • Are you sure those calls are actually returning anything? I.e., are you linked against Foundation? – jscs Jan 29 '19 at 19:10
  • @JoshCaswell Yes, both the `NSObject` and `NSAutoreleasePool` are valid objects (and yes I'm linked to foundation) – user1768788 Jan 29 '19 at 19:39
  • Interesting...thanks for confirming. – jscs Jan 29 '19 at 19:52
  • Oh, and not compiling with ARC enabled? – jscs Jan 29 '19 at 19:54
  • @JoshCaswell I'm not sure what you mean, I use `gcc -std=c99 test.c -framework Foundation`. I can also say that trying to release the objects after I've marked it to be autorelease (inside the loop), makes the app crash because of double de-allocation. – user1768788 Jan 29 '19 at 20:01
  • Ah, gcc, then yes, you are not using ARC! [ARC is a clang thing](https://clang.llvm.org/docs/AutomaticReferenceCounting.html#objective-c-automatic-reference-counting-arc). – jscs Jan 29 '19 at 20:02
  • Wait, then are you using GNUStep or Apple Foundation? – jscs Jan 29 '19 at 20:04
  • @JoshCaswell I think `gcc` is just `clang` on my machine. `gcc -help` or `gcc -v` claims to be the `clang LLVM compiler`. – user1768788 Jan 29 '19 at 20:12
  • Try compiling with `-fno-objc-arc` in that case? – jscs Jan 29 '19 at 20:21
  • @JoshCaswell No luck, still uses the same amount of memory. Occasionally only using 10mb but this seems to be random, happens with and without `-fno-objc-arc` – user1768788 Jan 29 '19 at 20:30
  • How are you measuring the memory usage of the process? Deallocating an object (or 1,000,000 objects) will generally **not** return the object's memory to the system. The memory will usually remain available in the process's address space to be recycled when new objects are later created. – rob mayoff Jan 29 '19 at 20:31
  • The code looks legit, all objects should be deallocated when the `drain` message is sent. I think you are experiencing some heap fragmentation issues, see for example https://stackoverflow.com/questions/9203677/diagnosing-heap-fragmentation-on-mac-os-x – Cristik Jan 29 '19 at 20:32
  • @robmayoff I'm looking at both activity monitor and using `top -pid ´pgrep -f a.out´ ` which seems to show the same values. Normally releasing the objects consistently lands below 1mb, it would be very strange if the system would act any (substantially) different when using autorelease. – user1768788 Jan 29 '19 at 20:40
  • BTW: When you call `objc_msgSend()` directly, you need to cast it to the same signature as the method you're calling. In this case, all the methods you're using happen to match the types of `objc_msgSend()`, so you can get away with it here. – bbum Jan 30 '19 at 00:32

1 Answers1

4

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.

rob mayoff
  • 375,296
  • 67
  • 796
  • 848
  • Ah, so it was behaving correctly, thank you! And thank you for telling me about Instruments, it looks really useful, I'm sure I will use it in the future. – user1768788 Jan 29 '19 at 21:00