Measuring Apple M4 cache latency from Rust

dakull1 pts0 comments

Measuring Apple M4 cache latency from Rust

Skip to content

The base M4 exposes enough information for macOS to report two CPU clusters, their L1 data caches, their shared L2 caches, and a 128-byte cache line. It does not expose a conventional L3 or System Level Cache capacity. A Rust microbenchmark can recover the CPU-visible cache boundaries, but it cannot turn an undocumented system cache into a trustworthy capacity number.

The experiment used one dependent randomized load per 128-byte line over working sets from 32 KiB to 128 MiB. Background quality of service kept the test on logical CPUs 0–5, while interactive quality of service kept it on CPUs 6–9. Those two passes reproduce the expected 64 KiB/4 MiB efficiency-cluster hierarchy and 128 KiB/16 MiB performance-cluster hierarchy.

The more interesting result is comparative. A simple midpoint marker places the broad E-cluster cache-to-memory transition near 6.8 MiB and the P-cluster transition near 26.7 MiB. The ratio is 3.93×, almost exactly the known 4× ratio between their shared L2 capacities. No second, common latency shoulder appears that can be assigned confidently to a separate SLC.

The defensible result: this benchmark measures CPU-visible effective cache behavior. It validates the documented L1/L2 structure and shows the cost of spilling beyond each cluster’s L2. It neither proves that the M4 lacks an SLC nor determines the SLC’s physical size.

The cache macOS does not name

The investigation began with an apparently simple hardware question: how large is the base M4’s System Level Cache?

On a conventional desktop processor, a tool may report private L1 and L2 caches followed by a shared L3. Apple silicon does not present its SoC that way. The CPU clusters have ordinary cache levels, while the larger SoC also contains fabric, memory controllers, a GPU, media blocks, neural hardware, and system-level caching behavior that Apple does not describe through a public capacity register.

sysctl reports the cluster L1 and L2 capacities. Queries for a conventional hw.l3cachesize or per-cluster L3 produced no value on this machine. That absence is not evidence that no other on-chip cache exists. It only means the operating system does not expose one through that interface.

A userspace timing curve can reveal boundaries in the path taken by CPU loads. It cannot name every physical structure participating in that path.

What the machine reports

The measurements ran on a Mac16,12 with a base Apple M4 and ten logical CPUs.

PropertyPerformance clusterEfficiency cluster

Logical CPUs46<br>L1 data cache128 KiB per core64 KiB per core<br>Shared cluster L216 MiB4 MiB<br>Cache-line size128 bytes

sysctl hw.model machdep.cpu.brand_string hw.ncpu \<br>hw.perflevel0.name hw.perflevel0.logicalcpu \<br>hw.perflevel0.l1dcachesize hw.perflevel0.l2cachesize \<br>hw.perflevel1.name hw.perflevel1.logicalcpu \<br>hw.perflevel1.l1dcachesize hw.perflevel1.l2cachesize \<br>hw.cachelinesize<br>These values provide two known reference points. If the microbenchmark cannot recover the 4 MiB and 16 MiB L2 behavior, it should not be trusted to infer an undocumented cache farther down the hierarchy.

Why a dependent pointer chase

A streaming loop is excellent for measuring bandwidth, but poor for isolating load latency. Modern cores can prefetch sequential addresses, issue several independent misses, and overlap memory work. The result then describes throughput across many in-flight requests rather than the time required by one load.

The probe instead creates one randomized cycle containing every cache line in the working set. Each node stores the index of the next node. The next address is unknown until the current load completes:

#[inline(never)]<br>fn chase(storage: &[usize], mut index: usize, iterations: u64) -> usize {<br>for _ in 0..iterations {<br>index = unsafe { *storage.get_unchecked(index) };<br>std::hint::black_box(index)<br>One node is placed at the beginning of each 128-byte cache line. A Fisher-Yates shuffle driven by a small deterministic generator establishes the traversal order. This does three useful things:

dependency prevents the core from issuing later loads before the current address is known;

random order makes ordinary sequential prefetching ineffective;

one complete cycle gives each cache line one visit before reuse.

The loop measures dependent-load latency, not cache bandwidth. Its absolute nanoseconds include the core’s clock state, QoS policy, translation overhead, interconnect, cache lookup, and memory service. Its strongest evidence is therefore the shape and location of repeatable transitions.

Separating E and P cores without affinity

macOS does not offer ordinary applications a portable “pin this thread to CPU 2” contract. It does provide quality-of-service classes. The benchmark uses pthread_set_qos_class_self_np with background QoS to favor the efficiency cluster and interactive QoS to favor the performance cluster.

QoS is a scheduler hint, not hard affinity, so the...

cache cluster line apple latency system

Related Articles