Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the limit on virtual memory for iOS?

Is there a cap on virtual memory usage on iOS? Figure there must be but haven't been able to track it down.

like image 907
Rob Segal Avatar asked Sep 19 '15 05:09

Rob Segal


People also ask

Does iOS have virtual memory?

Apple is adding virtual memory swap to iPadOS 16, allowing apps on the recent iPad Pro and iPad Air models to use free and available storage as extra memory for demanding workloads. With iPadOS 15, certain apps can use up to 12GB of memory on the highest-end M1 ‌iPad Pro‌ which has 16GB of total RAM.

Is there a limit to virtual memory?

Note: Microsoft recommends that virtual memory be set at no less than 1.5 times and no more than 3 times the amount of RAM on the computer. For power PC owners (most UE/UC users), there is likely at least 2 GB of RAM, so the virtual memory can be set up to 6,144 MB (6 GB).

How much RAM can an iOS app use?

Beta versions of iOS 15 and iPadOS 15 now give developers the option of requesting more RAM than the current 5GB maximum per app, with limitations. Apple has always set a cap on how much RAM any one app can use on the iPad, but it's become more of an issue as the devices themselves physically include more.

How much RAM does iOS 15 require?

iPadOS 15 Allows Apps to Use Up to 12GB of RAM on High-End iPad Pro, Up From Just 5GB. In June, we reported that starting with iPadOS 15, Apple is giving developers the ability to allocate their apps more RAM, allowing apps to use more of the available memory in the iPad to run faster and smoother.


2 Answers

tl;dr:

There are no direct or explicit virtual memory limits enforced by the OS, so your app could in theory allocate its entire potential logical address space. This size is is 4 gigabytes for 32 bit processors, and 18 exabytes for 64 bit processors.

Practically speaking, there are limitations in available virtual memory size which depend heavily on how your application uses its memory space. These limitations stem from inefficiencies in the way the OS manages virtual memory, as the OS is tuned aggressively for low power use, not heavy file I/O.

If you don't care about the finer details of these practical limits, feel free to stop reading here. Otherwise, what follows are my notes from researching a memory issue on iOS mixed with a primer on virtual memory and memory mapped files, and plenty of discussion on what makes iOS different from other OSes.


What's a Virtual Memory?

In modern operating systems, including iOS, all memory your process can access is technically virtual memory. It's called "virtual" memory because the address space exposed to the process (called the logical address space of the process) does not necessarily align with the physical address space of the machine, or even the virtual address space of other processes. Virtual memory allows for the kernel to provide different logical address spaces in different contexts.

On iOS, the size of this logical address space is in theory only limited by the pointer size, which is defined by the processor architecture. This means process has roughly 4 gigabytes of logical memory space on 32 bit processors, and 18 exabytes on 64 bit processors.

But my device only has 2GiB of RAM, how can I actually use 18 exabytes of memory space?

To answer this question, first you have to realize that when your process calls something like malloc to get a bit of memory, the kernel sets a side a bit of your process's logical address space and marks it allocated. In this case, this allocated memory also occupies physical memory space. Data stored in physical memory is called resident, in that it resides in physical memory, while the data which is resident is said to be part of the resident set. The kernel generally counts your physical memory usage against physical memory limits by tracking the resident set size for your process.

If this was the only way your process is allocating memory, then for most modern operating systems your process would be limited by the amount of unallocated physical memory, plus the amount of free space available in the machine's page file, swap partition, or some other nonvolatile backing store. However, iOS has no backing store and it enforces fairly strict constraints on the maximum resident set size of a process, so generally your app's resident set size will be limited by the lesser of the available physical memory and the per process resident set limit.

So if allocating memory doesn't ever let you approach the limit, why bother having 18 exabytes of address space per process?

One example of where this is useful is memory mapped files.

When you have an app that needs fast access to files and you want to rely on the kernel to figure out which bits of the file to read in and when, you can memory map the file (see documentation for mmap). In this case, the kernel presents the file (or the portion of the file you've requested) to your process as a contiguous region of the logical address space of the process.

How do memory mapped files work?

The OS will trap on accesses to the portion of the logical memory space reserved for the mapped file, and transparently copy chunks of your file into physical memory as they are accessed. These "chunks" are called pages, and the trap which allows the kernel to do this is called a page fault, while the act of making data resident is called paging in, and the removal of pages from memory is called paging out. This all makes it seem to your process like you have the region of the file which you have mapped (or the entire file, if that's what you mapped) in memory all the time. Of course, in order for this to work well, the OS must detect pages which haven't been accessed in a while, and page them out.

How does iOS mapped file support differ from other operating systems?

In the above scenario, iOS is a bit limited compared to most desktop class operating systems. Specifically, from what I can see in the documentation, it would appear that on iOS pages are only able purged from physical memory when they have been mapped read only, using the PROT_READ flag. This means that if you map a file with the PROT_WRITE flag, pages from this file will stay resident until you call munmap (I couldn't tell from the docs alone whether this means all pages will remain resident, or if it's just modified pages which stick around).

This behaviour deviates from other operating systems, as in the case of PROT_WRITE with MAP_SHARED, unmodified pages can be purged at any time, and modified pages may be purged once they are synchronized with the disk (aka, once they are no longer "dirty"). Also with other operating systems, when using PROT_WRITE and MAP_PRIVATE the case of unmodified pages remains the same, but modified pages can still be purged from physical memory so long as the OS is using some "backing store," such as a page file.

In the latter case, iOS can't page out these modifications as it has no backing store. I find the prior case confusing however, as in theory there's no logical limitation to allow synchronized pages to be paged out. The most I can infer is that in the PROT_WRITE and MAP_SHARED case, these pages are kept resident to avoid the need for tight synchronization between the I/O scheduler and pager in an SMP environment. This inference is supported by the weak suggestion in the documentation (link below) that on OSX, modified pages would be written to the backing store, regardless of whether or not they are synchronized with the disk. I intend to do a bit more reading on the Mach kernel to more fully understand these limitations, however.

So mmap with PROT_READ appears to be our path to using all of our process's available VM space without hitting resident set size limits, right? I mean, with this method you could in theory create read-only mappings of some large file many times over, with each mapping occupying a separate portion of your process's available logical memory space. On a 32bit OS, even on 32bit iOS, you can likely achieve your poorly-thought-out plans of world domination by virtual address space saturation quite easily. ENOMEM for everyone!

So if there aren't any virtual memory limits, why does my app get killed when I map read only files?

Practically speaking, while on many operating systems you can just mmap a file and rest easy while the OS figures out the details, this isn't really the case on iOS. Even if read-only mapped files dominate the logical address space of the process it's still possible that your app can be killed for going over resident set memory limits. This happens because of the way the kernel decides which pages can be removed from physical memory.

When reading this next part, remember that iOS has been tuned very aggressively for power efficiency. It's not a file server.

The kernel only frees pages which have been marked inactive, and I would infer from the docs linked below that pages are only able to be marked inactive if they have not been touched for some threshold period of time.

I would also infer from the docs and from my own experience that the piece of the kernel which is responsible for marking pages inactive does not coordinate well with the piece of the kernel which is responsible for checking resident set size limits.

As your resident set size approaches limits, the OS attempts to clear inactive pages, and fires off memory warnings, to which your app is expected to respond by reducing its resident set size. Once your resident set size crosses a certain threshold, jetsam kills your app.

So, given the above, what happens if the piece of the kernel which is responsible for marking pages inactive hasn't run in a while when your app is doing high-volume random access over a large number of pages?

Your app gets killed, even when the bulk of your resident set is occupied by pages of read-only mapped files.

I believe this is one of the primary reasons why apple recommends using smaller, more transient mappings where possible, instead of mapping the entire file (although in that usage pattern, memory space fragmentation starts to become a concern).

This answer wasn't ridiculously long enough! I demand more info!

If you'd like to know more, please see Apple's document on virtual memory in iOS and OSX, as well as the memory mapped files section of the File System Advanced Programming Topics doc, and the mmap man page for iOS.

As an aside, there are also some other related topics worth reading up on which make this all a bunch more complex and useful, such as page locking, page synchronization, I/O caching, prefetching, and for the deeply curious, how the OS accomplishes efficient multiprocessing access to shared mappings.

like image 55
Ben Burns Avatar answered Nov 16 '22 06:11

Ben Burns


The cap on an iPad Pro (12.9") with 4 GiB RAM running iOS 10.1.1 is approximately 4320-4325 MiB based on memory pressure testing on this device.

Note that 4320 MiB is more than the total physical memory (4194828288 bytes == ~4000.5 MiB), but far less than 18 exabytes.

Testing was performed by having a background process gradually allocate more and more memory until a memory warning was received. Depending on which test was run it would then either release that memory and stop, or else ignore the warnings and keep allocating until the app crashed.

The results were unexpected when watching RM (Resident Memory) alongside VM (Virtual Memory). RM would increase the same incremental amount as VM for a while, 1 MiB at a time, and then it would drop some amount (e.g. -18 MiB). This pattern repeated, and the peak RM value never came anywhere near the physical memory total.

Excerpt from one crash test:

VM: 4477714432 (4270 MiB), VM diff: 1048576 (1 MiB), RM: 1354809344 (1292 MiB), RM diff: 1048576 (1 MiB)
VM: 4478763008 (4271 MiB), VM diff: 1048576 (1 MiB), RM: 1355857920 (1293 MiB), RM diff: 1048576 (1 MiB)
VM: 4479811584 (4272 MiB), VM diff: 1048576 (1 MiB), RM: 1356906496 (1294 MiB), RM diff: 1048576 (1 MiB)
VM: 4480860160 (4273 MiB), VM diff: 1048576 (1 MiB), RM: 1357955072 (1295 MiB), RM diff: 1048576 (1 MiB)
VM: 4481908736 (4274 MiB), VM diff: 1048576 (1 MiB), RM: 1359003648 (1296 MiB), RM diff: 1048576 (1 MiB)
VM: 4482957312 (4275 MiB), VM diff: 1048576 (1 MiB), RM: 1360052224 (1297 MiB), RM diff: 1048576 (1 MiB)
VM: 4484005888 (4276 MiB), VM diff: 1048576 (1 MiB), RM: 1361100800 (1298 MiB), RM diff: 1048576 (1 MiB)
VM: 4485054464 (4277 MiB), VM diff: 1048576 (1 MiB), RM: 1344323584 (1282 MiB), RM diff: -16777216 (-16 MiB)
VM: 4486103040 (4278 MiB), VM diff: 1048576 (1 MiB), RM: 1345372160 (1283 MiB), RM diff: 1048576 (1 MiB)
VM: 4487151616 (4279 MiB), VM diff: 1048576 (1 MiB), RM: 1346420736 (1284 MiB), RM diff: 1048576 (1 MiB)
VM: 4488200192 (4280 MiB), VM diff: 1048576 (1 MiB), RM: 1347469312 (1285 MiB), RM diff: 1048576 (1 MiB)
VM: 4489248768 (4281 MiB), VM diff: 1048576 (1 MiB), RM: 1348517888 (1286 MiB), RM diff: 1048576 (1 MiB)
VM: 4490297344 (4282 MiB), VM diff: 1048576 (1 MiB), RM: 1349566464 (1287 MiB), RM diff: 1048576 (1 MiB)
VM: 4491345920 (4283 MiB), VM diff: 1048576 (1 MiB), RM: 1350615040 (1288 MiB), RM diff: 1048576 (1 MiB)
VM: 4492394496 (4284 MiB), VM diff: 1048576 (1 MiB), RM: 1351663616 (1289 MiB), RM diff: 1048576 (1 MiB)
VM: 4493443072 (4285 MiB), VM diff: 1048576 (1 MiB), RM: 1352712192 (1290 MiB), RM diff: 1048576 (1 MiB)
VM: 4494491648 (4286 MiB), VM diff: 1048576 (1 MiB), RM: 1353760768 (1291 MiB), RM diff: 1048576 (1 MiB)
VM: 4495540224 (4287 MiB), VM diff: 1048576 (1 MiB), RM: 1354809344 (1292 MiB), RM diff: 1048576 (1 MiB)
VM: 4496588800 (4288 MiB), VM diff: 1048576 (1 MiB), RM: 1355857920 (1293 MiB), RM diff: 1048576 (1 MiB)
VM: 4497637376 (4289 MiB), VM diff: 1048576 (1 MiB), RM: 1356906496 (1294 MiB), RM diff: 1048576 (1 MiB)
VM: 4498685952 (4290 MiB), VM diff: 1048576 (1 MiB), RM: 1357955072 (1295 MiB), RM diff: 1048576 (1 MiB)
VM: 4499734528 (4291 MiB), VM diff: 1048576 (1 MiB), RM: 1359003648 (1296 MiB), RM diff: 1048576 (1 MiB)
VM: 4500783104 (4292 MiB), VM diff: 1048576 (1 MiB), RM: 1360052224 (1297 MiB), RM diff: 1048576 (1 MiB)
VM: 4501831680 (4293 MiB), VM diff: 1048576 (1 MiB), RM: 1361100800 (1298 MiB), RM diff: 1048576 (1 MiB)
VM: 4502880256 (4294 MiB), VM diff: 1048576 (1 MiB), RM: 1362149376 (1299 MiB), RM diff: 1048576 (1 MiB)
VM: 4503928832 (4295 MiB), VM diff: 1048576 (1 MiB), RM: 1363197952 (1300 MiB), RM diff: 1048576 (1 MiB)
VM: 4504977408 (4296 MiB), VM diff: 1048576 (1 MiB), RM: 1347469312 (1285 MiB), RM diff: -15728640 (-15 MiB)
VM: 4506025984 (4297 MiB), VM diff: 1048576 (1 MiB), RM: 1348517888 (1286 MiB), RM diff: 1048576 (1 MiB)
VM: 4507074560 (4298 MiB), VM diff: 1048576 (1 MiB), RM: 1349566464 (1287 MiB), RM diff: 1048576 (1 MiB)
VM: 4508123136 (4299 MiB), VM diff: 1048576 (1 MiB), RM: 1350631424 (1288 MiB), RM diff: 1064960 (1 MiB)
VM: 4509171712 (4300 MiB), VM diff: 1048576 (1 MiB), RM: 1351680000 (1289 MiB), RM diff: 1048576 (1 MiB)
VM: 4510220288 (4301 MiB), VM diff: 1048576 (1 MiB), RM: 1352728576 (1290 MiB), RM diff: 1048576 (1 MiB)
VM: 4511268864 (4302 MiB), VM diff: 1048576 (1 MiB), RM: 1353777152 (1291 MiB), RM diff: 1048576 (1 MiB)
VM: 4512317440 (4303 MiB), VM diff: 1048576 (1 MiB), RM: 1354825728 (1292 MiB), RM diff: 1048576 (1 MiB)
VM: 4513366016 (4304 MiB), VM diff: 1048576 (1 MiB), RM: 1355874304 (1293 MiB), RM diff: 1048576 (1 MiB)
VM: 4514414592 (4305 MiB), VM diff: 1048576 (1 MiB), RM: 1356922880 (1294 MiB), RM diff: 1048576 (1 MiB)
VM: 4515463168 (4306 MiB), VM diff: 1048576 (1 MiB), RM: 1357971456 (1295 MiB), RM diff: 1048576 (1 MiB)
VM: 4516511744 (4307 MiB), VM diff: 1048576 (1 MiB), RM: 1359020032 (1296 MiB), RM diff: 1048576 (1 MiB)
VM: 4517560320 (4308 MiB), VM diff: 1048576 (1 MiB), RM: 1360068608 (1297 MiB), RM diff: 1048576 (1 MiB)
VM: 4518608896 (4309 MiB), VM diff: 1048576 (1 MiB), RM: 1361117184 (1298 MiB), RM diff: 1048576 (1 MiB)
VM: 4519657472 (4310 MiB), VM diff: 1048576 (1 MiB), RM: 1362165760 (1299 MiB), RM diff: 1048576 (1 MiB)
VM: 4520706048 (4311 MiB), VM diff: 1048576 (1 MiB), RM: 1363214336 (1300 MiB), RM diff: 1048576 (1 MiB)
VM: 4521754624 (4312 MiB), VM diff: 1048576 (1 MiB), RM: 1364262912 (1301 MiB), RM diff: 1048576 (1 MiB)
VM: 4522803200 (4313 MiB), VM diff: 1048576 (1 MiB), RM: 1365311488 (1302 MiB), RM diff: 1048576 (1 MiB)
VM: 4523851776 (4314 MiB), VM diff: 1048576 (1 MiB), RM: 1366360064 (1303 MiB), RM diff: 1048576 (1 MiB)
VM: 4524900352 (4315 MiB), VM diff: 1048576 (1 MiB), RM: 1367408640 (1304 MiB), RM diff: 1048576 (1 MiB)
VM: 4525948928 (4316 MiB), VM diff: 1048576 (1 MiB), RM: 1347715072 (1285 MiB), RM diff: -19693568 (-18 MiB)
VM: 4526997504 (4317 MiB), VM diff: 1048576 (1 MiB), RM: 1348616192 (1286 MiB), RM diff: 901120 (0 MiB)
VM: 4528046080 (4318 MiB), VM diff: 1048576 (1 MiB), RM: 1349664768 (1287 MiB), RM diff: 1048576 (1 MiB)
VM: 4529094656 (4319 MiB), VM diff: 1048576 (1 MiB), RM: 1350713344 (1288 MiB), RM diff: 1048576 (1 MiB)
VM: 4530143232 (4320 MiB), VM diff: 1048576 (1 MiB), RM: 1351761920 (1289 MiB), RM diff: 1048576 (1 MiB)
VM: 4531191808 (4321 MiB), VM diff: 1048576 (1 MiB), RM: 1352810496 (1290 MiB), RM diff: 1048576 (1 MiB)
Message from debugger: Terminated due to memory issue

This behavior looks an awful lot like VM swapping to me.

UPDATE: As an experiment, performed a revised test with memset commented out so the memory is allocated but not filled with any values.
--> Memory pressure test never received a warning and crashed at VM: 8979611648 (8563 MiB), RM: 194134016 (185.1 MiB)

maia(2943,0x256573000) malloc: *** mach_vm_map(size=10485760) failed (error code=3) *** error: can't allocate region

like image 38
jk7 Avatar answered Nov 16 '22 05:11

jk7