Apple Silicon uses 16 KB memory pages, and it changes your arithmetic
Every guide that tells you to multiply vm_stat page counts by 4096 was written for Intel Macs. On Apple Silicon the page is four times larger, and using the wrong constant understates your memory by 75 percent.
The first line vm_stat prints is the one almost everybody scrolls past:
Mach Virtual Memory Statistics: (page size of 16384 bytes)
Sixteen kilobytes. Every Intel Mac used 4,096 bytes, and a large amount of still-current advice about reading macOS memory statistics was written against that number. Applying it here understates every figure by 75 percent.
Confirm it yourself
sysctl hw.pagesize hw.memsize
hw.pagesize: 16384
hw.memsize: 25769803776
Divide one by the other and this machine holds 1,572,864 pages. On a hypothetical 4 KiB system with the same 24 GiB it would hold 6,291,456.
What a page is
Physical memory is not handed to programs byte by byte. It is divided into fixed-size blocks, and a page is the smallest unit the memory system tracks, protects and moves. Every allocation rounds up to a whole number of pages. Every permission boundary falls on a page boundary. Every entry in the page table, the structure translating the addresses a program uses into physical locations, describes one page.
That last point is where the size choice pays.
Why four times larger
A page table needs one entry per page. Quadrupling the page size divides the number of entries by four.
The translation lookaside buffer, a small cache of recent address translations sitting in the critical path of essentially every memory access, holds a fixed number of entries. With 16 KiB pages each entry covers four times as much memory, so the same TLB spans four times the address range before it starts missing. A TLB miss costs a walk through the page table in memory, and avoiding those is one of the cheapest performance wins available to a chip designer.
The trade is internal fragmentation. A program that needs 16,385 bytes gets two pages, 32,768 bytes, and wastes 16,383 of them. Across many small allocations that adds up. Apple evidently concluded the translation win outweighs it, and given that every Apple Silicon Mac ships this way, the conclusion is settled.
The error the wrong constant produces
Take the wired figure from a real vm_stat sample on this machine: 195,479 pages.
195,479 × 4,096 = 800,681,984 bytes = 763 MiB wrong
195,479 × 16,384 = 3,202,727,936 bytes = 2.98 GiB right
The wrong answer is not slightly wrong. It is a quarter of the truth, and it is plausible enough to pass unnoticed: 763 MiB of wired memory sounds entirely reasonable for a Mac. Nothing about the result announces itself as an error, which is what makes this worth stating explicitly.
Any script, monitoring dashboard or article that hardcodes 4096 produces figures like this. The fix is to never hardcode it:
PAGE=$(sysctl -n hw.pagesize)
WIRED=$(vm_stat | awk '/Pages wired down/ {gsub(/\./,""); print $4}')
echo $(( WIRED * PAGE / 1024 / 1024 )) MiB wired
Where else the size shows up
The page size is not confined to memory statistics. It determines the granularity of mmap, so a memory-mapped file consumes memory in 16 KiB units. It determines the minimum stack size for a thread. It sets the alignment requirement for anything using page-level protection, which includes JIT compilers marking regions executable.
Software written on the assumption of 4 KiB alignment, particularly runtimes and virtualisation tools ported from x86, is the category that historically broke on Apple Silicon for reasons that looked mysterious until the page size was accounted for.
The one-line version
Read the page size, do not assume it. vm_stat prints it in its own header specifically so you do not have to guess, and on Apple Silicon the guess most people have inherited is wrong by a factor of four.
Where the assumption is baked in
The 4,096 constant is not only in blog posts. It appears in a surprising number of places that are easy to inherit without noticing.
Monitoring scripts are the obvious case. Anything parsing vm_stat output and converting to
bytes needs the page size, and a script written on Linux, where 4 KiB is nearly universal, will
produce numbers that are quietly a quarter of the truth on a Mac.
Language runtimes are the subtler case. Several allocators and garbage collectors are tuned around an assumed page size, and a runtime that assumes 4 KiB on a 16 KiB system will round its own arenas to the wrong boundary. This is usually a performance question rather than a correctness one, and it is one of the reasons some cross-platform runtimes took a while to reach parity on Apple Silicon.
Anything using mmap directly hits it hardest. Mapping a file is done in page units, so a
program requesting a 4,096 byte mapping receives 16,384 bytes of address space whether it wants
them or not, and code that assumes the returned region ends where it asked can read past its own
intent without faulting.
# What the system will actually give you
getconf PAGESIZE
sysctl -n hw.pagesize
Both report 16384. getconf is the portable spelling and works on Linux too, which makes it the
better choice in a script that has to run in both places.
The TLB argument, with numbers
The reason for the larger page is worth making concrete.
A translation lookaside buffer caches recent virtual-to-physical address translations. Suppose a core has 128 entries for data. With 4 KiB pages those entries cover:
128 x 4,096 = 524,288 bytes = 512 KiB
With 16 KiB pages the same 128 entries cover:
128 x 16,384 = 2,097,152 bytes = 2 MiB
Four times the reach, with no additional silicon. A program whose working set fits inside 2 MiB now runs without TLB misses where previously it would have missed continuously. Since a miss costs a walk through the page table in memory, and that walk is itself several dependent memory accesses, the saving is large and it applies to essentially every workload rather than to a particular category.
That is the trade Apple took: some wasted memory at allocation boundaries in exchange for address translation that stops being a bottleneck.
Measuring the fragmentation cost
The cost side is real but small, and it is possible to see it. Every allocation rounds up to a whole page, so the waste for any given allocation is between zero and 16,383 bytes.
A process making many small independent allocations pays more of this than one making few large ones. In practice most allocations do not go directly to the kernel: the C library’s allocator requests memory in large chunks and subdivides them itself, which absorbs most of the rounding before the page size is involved at all.
Where it does show is in process count. Every process needs at least one page for each of its
distinct memory regions, so a system running many small processes carries more overhead than one
running a few large ones. On a machine with 548 processes, as this one had when sampled, that is
not nothing, and it is part of why wired memory on a modern Mac is measured in gigabytes.
The rule to carry
Never hardcode a page size. Read it:
PAGE=$(sysctl -n hw.pagesize)
Two characters more than typing 4096, and it is correct on Intel Macs, on Apple Silicon, on Linux, and on whatever comes next. The reason this is worth stating so plainly is that the wrong answer never announces itself. A memory figure that is a quarter of the truth still looks like a plausible memory figure, and you will believe it.
Checking a script you already have
If you maintain anything that reads vm_stat, this finds the assumption:
grep -rn "4096\|4 \* 1024" ~/bin ~/.zshrc 2>/dev/null
Any hit near a memory calculation is worth replacing with $(sysctl -n hw.pagesize). The reason
to go looking rather than waiting for a wrong number to announce itself is that it will not: a
figure one quarter of the truth still looks like a plausible memory figure.
Takeaways
- hw.pagesize reports 16,384 bytes on Apple Silicon against 4,096 on Intel Macs.
- Multiplying vm_stat counts by 4096 on an Apple Silicon Mac understates every figure by exactly 75 percent.
- A 24 GiB machine holds 1,572,864 pages rather than the 6,291,456 it would need at 4 KiB, so the page table is a quarter of the size.
- The cost is internal fragmentation: a process needing one byte more than a page boundary consumes another 16 KiB.
Questions
- How do I check my own page size?
- Run sysctl hw.pagesize, or read the first line of vm_stat output, which states it explicitly. Never assume it.
- Does the larger page waste memory?
- It costs some through internal fragmentation, because an allocation that overruns a boundary by one byte still consumes a whole further page. In exchange the page table shrinks to a quarter and address translation gets faster, which Apple evidently judged the better trade.
- Do Rosetta and x86 software see 16 KB pages too?
- Rosetta 2 translates x86 code but the underlying hardware page size does not change. Software that hardcodes 4096 rather than querying the system is the category that runs into trouble.
Sources
What we read. Distinct from what we measured, which is in the article itself.