Mixed Memories
Why do macOS programs show different amounts of used memory?
9 Jul 2023
For most Mac users, determining the amount of memory used is pretty simple. You go into Activity Monitor and have a look at the Memory tab.
The more technically inclined might go one step further, and use something like iStat Menus or neofetch
. I'm too cheap to buy a license for the former, so let's have a look at the latter.
But they'll quickly notice a small problem: the amount of used memory can sometimes be different.1 And while in this case, one is in mebibytes and the other in megabytes,2 the difference still doesn't line up when you do the conversion. So let's go one step lower and query the amount of memory by compiling a little executable in a trendy programming language such as Rust.
use sysinfo::{System, SystemExt};
fn main() -> () {
let sys = System::new_all();
print!(
"{mem_used} bytes / {mem} bytes",
mem_used = sys.used_memory(),
mem = sys.total_memory()
);
}
Compiling test_crate v0.0.1 (file path redacted)
Finished dev [unoptimized + debuginfo] target(s) in 0.26s
Running `target/debug/test_crate`
15674580992 bytes / 17179869184 bytes
Well, that isn't helpful. Let's actually have a look at going on here. Unfortunately, Activity Monitor isn't open source,3 but we can have a look at the internals of the neofetch
source code and the sysinfo
crate I used. I'm going to start with the former.
At its core, Neofetch is a one-file Bash script that handles all its functionality for a variety of (largely Unix-like, but also Windows) operating systems. It is divided into different functions, each of which is used to obtain the appropriate statistics for the respective category. We want the get_memory()
function. It's divided into cases for different operating systems, so we'll grab the section of code that gets the macOS info.
"Mac OS X" | "macOS" | "iPhone OS")
mem_total="$(($(sysctl -n hw.memsize) / 1024 / 1024))"
mem_wired="$(vm_stat | awk '/ wired/ { print $4 }')"
mem_active="$(vm_stat | awk '/ active/ { printf $3 }')"
mem_compressed="$(vm_stat | awk '/ occupied/ { printf $5 }')"
mem_compressed="${mem_compressed:-0}"
mem_used="$(((${mem_wired//.} + ${mem_active//.} + ${mem_compressed//.}) * 4 / 1024))"
;;
As one can see, in typical Unix fashion, it calls a number of small utilities to gather the amount of used memory and total memory. In this function, they are sysctl
, which is a command-line tool that allows querying system statistics, and vm_stat
, which lists off various Mach kernel4 memory statistics.
First, it grabs the grabs the total memory and divides it twice to convert it into mebibytes. It then pulls together wired memory, active memory, and compressed memory to compute the used memory. Since the figures it uses are in pages, it then uses the page size5 to convert these pages into bytes, and then divides it as it did with the total memory.
Now, let's look at the Rust sysinfo
crate I used earlier:
Much of the library is composed of OS-specific code that lives in its respective folder. In our case, that's src/apple
, for macOS and iOS code. In particular, we want the functions under SystemExt
, so we'll grab src/apple/system.rs
.
// get ram info
if self.mem_total < 1 {
get_sys_value(
libc::CTL_HW as _,
libc::HW_MEMSIZE as _,
mem::size_of::<u64>(),
&mut self.mem_total as *mut u64 as *mut c_void,
&mut mib,
);
}
let mut count: u32 = libc::HOST_VM_INFO64_COUNT as _;
let mut stat = mem::zeroed::<vm_statistics64>();
if host_statistics64(
self.port,
libc::HOST_VM_INFO64,
&mut stat as *mut vm_statistics64 as *mut _,
&mut count,
) == libc::KERN_SUCCESS
{
// From the apple documentation:
//
// /*
// * 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.
// */
self.mem_available = u64::from(stat.free_count)
.saturating_add(u64::from(stat.inactive_count))
.saturating_mul(self.page_size_kb);
self.mem_free = u64::from(stat.free_count)
.saturating_sub(u64::from(stat.speculative_count))
.saturating_mul(self.page_size_kb);
}
Further down, the used memory is computed by subtracting mem_free
from mem_total
. Makes sense, I guess. As for the calculations themselves, they are obtained via macOS system calls, which is more efficient than launching a number of Unix command-line processes. The total memory is retreived using the same system call as Neofetch (hw.memsize
), but it uses the system function sysctlbyname
directly, rather than the intermediary sysctl
terminal utility. Obtaining the free memory is done with the host_statistics64
function, which is what vm_stat
uses underneath the hood as well. However, what it does with those values is different than Neofetch: the crate obtains the system's internal count of free pages, and then (saturatedly) subtracts speculative pages from them.
Speculative pages hold data grabbed ahead of time that the kernel thinks currently running processes might need later. But as the comment in the code says, they aren't used by anyone yet, and they've also pretty easily evicted if the memory is needed for more important uses, so they aren't really "used" in a way that causes problems. In other words, the Rust sysinfo
crate's quite a bit stricter in its definition of what counts as "free" memory: any page that is occupied so far, even if it's not actually used by a program yet, is considered "used". The line between free and used memory is blurrier and more subjective than it may initially appear. So let's clear few things up.
- Memory size, as you probably know if you've reading this, is the amount of memory (to be specific, RAM, or short-term data, as opposed to files on disk) a computer can store. It can be obtained via
sysctl -n hw.memsize
. - Page size is how big a page is in bytes. On Intel Macs, it's 4096 bytes (4 KiB); Apple silicon Macs are four times larger at 16 KiB. It can be obtained via
sysctl -n hw.pagesize
or the top line ofvm_stat
.
- Free memory in technical terms is memory that is being used for absolutely nothing.6 It can be obtained by
vmstat
's "Pages free"7 section orsysctl -n vm.page_free_count
(likesysinfo
, these counts exclude speculative memory). - Speculative memory is, as stated above, memory that the kernel thinks currently obtained processes may need later, but can be easily evicted for more space. It can be obtained by
vmstat
's "Pages speculative" section, orsysctl -n vm.page_speculative_count
. - Wired memory is memory that is used by the kernel to run essential system processes. It is essentially occupied by the kernel and cannot be freed. It can be obtained by
vmstat
's "Pages wired down" section, or "Wired memory" under Activity Monitor's Memory tab. - Active memory is memory that is being used right now. As in, right this moment, while you've reading this. It can be obtained by
vmstat
's "Pages active" section. - Inactive memory is memory that isn't used right now, but could be needed, and again can be evicted for more space if necessary. It can be obtained by
vmstat
's "Pages inactive" section.- Compressed memory is inactive memory that is compressed to save RAM space. It can be obtained by
vmstat
's "Pages occupied by compressor" section, or "Compressed" under Activity Monitor's Memory tab.
- Compressed memory is inactive memory that is compressed to save RAM space. It can be obtained by
- Anonymous memory is memory that isn't connected to a file on the file system (i.e. is manually allocated by applications). It can be obtained using
vmstat
's "Anonymous pages" section, and is roughly equivalent tosysctl -n vm.page_pageable_internal_count
. - File-backed memory is the opposite of anonymous memory: memory that is connected to a file. It can be obtained by
vmstat
's "File-backed pages" section, or "Cached Files" under Activity Monitor's Memory tab.
- Purgeable memory is memory that is designated by app developers as being safely freeable if the OS requires more space (and recreated again later when needed). It can be obtained via
vmstat
's "Pages purgeable" section. - Pageable memory is memory that can be paged to disk (swap) if the OS runs out of space and no other memory can be freed. In systems with sufficient memory, paging is very rare, but the classfication is still used internally. It can be obtained via
sysctl -n vm.page_pageable_internal_count
andsysctl -n vm.page_pageable_external_count
. Swap is available as "Swap Used" under Activity Monitor.
- Application memory is a high-level construct used by Activity Monitor that approximates core memory used by apps. Although the formula isn't publicly available, it appears to be anonymous memory that isn't purgeable.
The issue is obvious: different sources calculate the memory different ways. Neofetch adds wired, active, and compressed memory. The Rust sysinfo
crate gives everything that is not strictly free. And Activity Monitor, although its' method isn't publicly available, seems to add application memory, wired memory, and compressed memory. As for which one is the most accurate, it depends on what you mean really. Perhaps sysinfo
is the most strictly accurate, but it's very unhelpful for day-to-day usage.
-
1
This has actually been rectified in the Neofetch codebase, but a new version hasn't been released for quite a while.
-
2
Some people define storage sizes in terms of 1024-byte multiples, while others use a system based on 1000 bytes. Usually, this tends to depend on whether they have an incentive to show a higher number of KBs, GBs, MBs, and such. To stop confusion, standards bodies have redefined 1024-byte units as mebibytes (MiB), kibibytes (KiB), gibibytes (GiB), etc - though many manufacturers still don't adhere to such a distinction.
-
3
Shocker!
-
4
Mach is the name of the kernel that was eventually developed into the XNU kernel behind macOS. One of the key tasks behind a kernel like it is to allocate and manage memory.
-
5
Memory is divided up into units called pages internally by the operating system.
-
6
<insert tumbleweed gif>
-
7
The numbers in
vm_stat
are the amount of pages, while numbers obtained bysysctl
are usually either bytes or pages.