in iOS

Where’d That Memory Go?

My reiphone_memorycent rant about the dismal situation of memory in the iPhone quickly became quite popular. Most people were either unaware of the situation, or completely agreed that it was a major stumbling block for any app that tries to make good use of the hardware.

As a response to that post, some people suggested some intriguing ways to increase the available memory on the iPhone. I’ve been experimenting with that a bit, but I don’t have any conclusive solutions yet. Right now it’s all totally unreliable hacks. Hopefully in a followup post I can present some solutions.

In the meanwhile, I wanted to talk about a piece of the puzzle, which is keeping track of the memory status on the iPhone. How much memory is available? How much memory does the program use? We need to be able to answer those questions accurately in order to do anything about memory. Unfortunately, there doesn’t appear to be a good answer even for this!

Finding Total Memory

Let’s start with the easy part: Finding the overall memory on the device. Right now all iPhones and iPod Touch models have 128MB RAM, so it’s kind of pointless, but it’s interesting anyway. To accomplish that we need to dig into some of the low-level functions that query hardware capabilities. In particular, the function sysctl() reports both physical memory and user memory.

    int mem;
    int mib[2];
    mib[0] = CTL_HW;
    mib[1] = HW_PHYSMEM;
    size_t length = sizeof(mem);
    sysctl(mib, 2, &mem, &length, NULL, 0);
    NSLog(@"Physical memory: %.2fMB", mem/1024.0f/1024.0f);

    mib[1] = HW_USERMEM;
    length = sizeof(mem);
    sysctl(mib, 2, &mem, &length, NULL, 0);
    NSLog(@"User memory: %.2fMB", mem/1024.0f/1024.0f);

The output of that code is:

Physical memory: 116.00MB
User memory: 91.30MB

Interesting. Physical memory is not quite reported as 128MB. That’s probably because the video memory takes up 12MB of the total. The kernel apparently uses 24.7MB, which is not unreasonable if that was all the memory used by the OS. Unfortunately, that’s only for the kernel. The different processes and apps are going to use up more than that. A LOT more than that.

Incidentally, sysctl() is a pretty rocking function. It will give you all sorts of cool information, such as CPU and bus frequency (412MHz and 103MHz respectively for my iPhone 3G), cache sizes, and whether a vector unit is present (it claims it isn’t, so it must not be referring to the vfp).

Finding Available Memory

As cool a function as it is, sysctl() only returns static information about the hardware. If we want to find information about the current status of the memory, we need to look elsewhere.

As far as I can tell, the best function to get this information host_statistics() with HOST_VM_INFO_COUNT to get virtual memory statistics. Querying that function fills out the following structure:

struct vm_statistics {
    natural_t	free_count;		/* # of pages free */
    natural_t	active_count;		/* # of pages active */
    natural_t	inactive_count;		/* # of pages inactive */
    natural_t	wire_count;		/* # of pages wired down */
    natural_t	zero_fill_count;	/* # of zero fill pages */
    natural_t	reactivations;		/* # of pages reactivated */
    natural_t	pageins;		/* # of pageins */
    natural_t	pageouts;		/* # of pageouts */
    natural_t	faults;			/* # of faults */
    natural_t	cow_faults;		/* # of copy-on-writes */
    natural_t	lookups;		/* object cache lookups */
    natural_t	hits;			/* object cache hits */

    /* added for rev1 */
    natural_t	purgeable_count;	/* # of pages purgeable */
    natural_t	purges;			/* # of pages purged */

    /* added for rev2 */
    /*
     * NB: speculative pages are already accounted for in "free_count",
     * so "speculative_count" is the number of "free" pages that are
     * used to hold data that was read speculatively from disk but
     * haven't actually been used by anyone so far.
     */
    natural_t	speculative_count;	/* # of pages speculative */
};

Lots of good info there! (at least for a hardware geek like me). The most interesting one is free_count. That number combined with the page size (which is defined in the handy global variable vm_page_size–which is unsurprisingly 4K on the iPhone) should give us the amount of available memory, right? Right?

Kind of. Unfortunately virtual memory is managed at many different levels. The OS will keep some pages on reserve to use on a rainy day. So it’s possible that there is more memory available than reported in this function. It will however give you a good minimal bound on the amount of free memory. You can count on that amount for sure.

This function puts it together to return the amount of available memory in KB:

int getAvailableMemoryInKB()
{
    vm_statistics_data_t vmStats;
    mach_msg_type_number_t infoCount = HOST_VM_INFO_COUNT;
    kern_return_t kernReturn = host_statistics(mach_host_self(),
                             HOST_VM_INFO, (host_info_t)&vmStats, &infoCount);
    if(kernReturn != KERN_SUCCESS)
        return -1;
    return (vm_page_size * vmStats.free_count) / 1024;
}

To check that available memory roughly matches what I do in the app, I allocate a 1MB block, run it again and… surprise! I get the same amount of memory available. Apparently the OS is waiting to actually set those memory pages aside until you really need them (I hate it when machines try to out-think me). So to really get them to count as allocated, we need to write to every page of the memory we just allocated. The easiest way is just go bzero(mem, 1024*1024);. Running the function again correctly shows that the available memory has gone down by 1MB.

(Not) Making Sense Of Used and Total Memory

An old habit I picked up during my engineering days is to always confirm my calculations through a different path. If the total amount of memory reported by the virtual memory system matched up with the total amount of memory reported by sysctl() I would leave this happy and be able to sleep soundly tonight.

The total amount of virtual memory is calculated by adding together the free, used, wired, and inactive pages. It should come up to be roughly around 91MB. The answer: 89MB. OK, close enough, right? Not really.

Here’s the biggest mystery so far: If I malloc a chunk of memory (and write to it to have it marked as not available), host_statistics() correctly shows that those pages are not available anymore, but they don’t show up as used, wired, or anything else!!! So simply adding free, used, wired, and inactive reports a totally bogus number that doesn’t take into account allocations from your program. What’s going on there? Obviously I’m misunderstanding something, so maybe someone with more knowledge of kernel and vm features can help me out. Otherwise I won’t be getting much sleep I’m afraid 🙂

Just to prove that I’m not crazy, here’s a code snippet and its output:

- (void)updateStatus
{
	vm_statistics_data_t vmStats;
	mach_msg_type_number_t infoCount = HOST_VM_INFO_COUNT;
	host_statistics(mach_host_self(), HOST_VM_INFO, (host_info_t)&vmStats, &infoCount);

	const int totalPages = vmStats.wire_count + vmStats.active_count +
                               vmStats.inactive_count + vmStats.free_count;
	const int availablePages = vmStats.free_count;
	const int activePages = vmStats.active_count;
	const int wiredPages = vmStats.wire_count;
	const int purgeablePages = vmStats.purgeable_count;

	NSMutableString* txt = [[NSMutableString alloc] initWithCapacity:512];
	[txt appendFormat:@"Total: %d (%.2fMB)", totalPages, pagesToMB(totalPages)];
	[txt appendFormat:@"nAvailable: %d (%.2fMB)", availablePages, pagesToMB(availablePages)];
	[txt appendFormat:@"nActive: %d (%.2fMB)", activePages, pagesToMB(activePages)];
	[txt appendFormat:@"nWired: %d (%.2fMB)", wiredPages, pagesToMB(wiredPages)];
	[txt appendFormat:@"nPurgeable: %d (%.2fMB)", purgeablePages, pagesToMB(purgeablePages)];

	NSLog(txt);
	[txt release];
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    [self updateStatus];
    const int sizeInBytes = 1024*1024;
    int* mem = (int*)malloc(sizeInBytes);
    bzero(mem, sizeInBytes);
    [self updateStatus];
}

And the output is:

Total: 22128 (86.44MB)
Available: 8124 (31.73MB)
Active: 6361 (24.85MB)
Wired: 6408 (25.03MB)
Purgeable: 359 (1.40MB)

Total: 21880 (85.47MB)
Available: 7888 (30.81MB)
Active: 6344 (24.78MB)
Wired: 6412 (25.05MB)
Purgeable: 359 (1.40MB)

Notice how the available pages go down by about 1MB but everything else stays about the same?

While we’re at it, does someone know of a better, lower-level way to allocate memory than malloc()? I’d like to just allocate memory pages directly. I looked into vm_allocate() but I wasn’t able to get the correct port rights. Anyone?

Update: Thanks to Colin for pointing out that mmap is the low-level memory allocation function I was looking for. You can allocate pages directly without going through malloc. Perfect!

Next Up…

This is a stepping stone towards the memory experiments I’m running. In the next day or so I should be able to report some good ways to clear up as much memory as possible for our apps.

  • Pingback: Antair Games » Blog Archive » Weekly Update()

  • Nathan

    It’s just a guess, but it looks like you’re not taking into account the zero_fill_count from the VM statistics struct. Considering that you’re using bzero() (which I’m guessing writes zeros to memory) to touch the memory you allocate, it seems possible that the OS might mark the pages as being zero filled rather than putting them in some other category.

    Let me know if I’m missing something. I know next to nothing about iPhone and OSX development, so it’s quite possible.

  • Nathan, Good guess, but no cigar! I would have been very surprised if somehow the virtual memory system knew that you had filled a full page with zeroes and added it to a different pool. I think that zero_fill_count must be for pages that are flagged in some special ways.

    I haven’t able to find good documentation on this, but zero_fill_count doesn’t even appear to be a count of physical pages. When I run it on the iPhone it reports to have 300,000+ zero_fill_count. So maybe that’s the number of pages to fill in the gaps between different real allocations? In any case, it seems it’s not the answer to this riddle.

    Just to double-triple check, I changed bzero to memset(mem, 0xCD, size) and I still have the same problem of disappearing vm pages.

  • Colin Barrett

    Excellent series of articles Noel. With regard to the ‘lower-level way to allocate memory’ – have you tried an anonymous mmap? Something like this:

    void *mem = mmap(0, mem_bytes, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON, -1, 0);

    You’ll need to include sys/mman.h. This works on OSX proper – I haven’t tried it on the iPhone.

  • Thanks, Colin! I didn’t know about mmap, but that’s exactly what I was looking for. It works perfectly on the iPhone and allows me to allocate pages directly without going through the indirections of malloc. Perfect!