You have
That single shared address space is a wonderful programming abstraction — but the hardware underneath is not uniform. The crucial question of this lesson is: does every core reach every byte of memory in the same amount of time? If yes, you have UMA; if no, you have NUMA, and ignoring the difference can quietly halve your program's speed.
In a Uniform Memory Access machine — historically called a symmetric multiprocessor (SMP) — all cores share a single pool of main memory through a common interconnect, and the latency to any address is the same from every core. It is symmetric: no core is privileged, no address is "closer". This is the mental model most programmers carry, and it is exactly right for a small chip.
The beauty of UMA is its simplicity: the operating system can place a thread on any core and any data anywhere, and performance does not care. The catch is that the single shared path to memory is a bottleneck. Add enough cores and they all contend for the same bus and the same memory controller; the interconnect saturates and everyone waits. UMA does not scale to dozens of sockets.
To scale past a handful of cores, big systems split memory into pieces and attach each piece directly to a
group of cores — a socket or NUMA node. Now a core's local memory
is fast, but reaching another node's memory means a hop across an inter-socket link
(Intel's UPI, AMD's Infinity Fabric, the old QPI). That remote access is real, physical, and slower —
typically
The address space is still shared — a core can read any byte on any node with a plain load — but access is now Non-Uniform. Two identical loads can take wildly different times depending on where the data physically lives relative to the core running the code. That is the whole idea of NUMA, and it turns memory placement into a first-class performance concern.
Suppose local memory takes
where
Operating systems fight remote access with two tricks. Affinity pins a thread to the cores of one node so it does not wander away from its data. And the first-touch policy allocates each memory page on the node of whichever core first writes to it — so if a thread initialises its own data, that data lands next door. The classic NUMA bug is to have one setup thread touch a giant array (parking it all on node 0) and then hand slices to worker threads scattered across every node — now most workers read remotely. The fix is to let each worker initialise its own slice.
| UMA (SMP) | NUMA | |
|---|---|---|
| Memory latency | same from every core | local fast, remote slower |
| Scales to | a few cores / one socket | many cores / many sockets |
| Bottleneck | the one shared path to memory | the inter-socket links |
| Placement matters? | no | yes — affinity & first-touch |
| Typical use | phones, laptops, small chips | multi-socket servers, big EPYC/Xeon |
You might think NUMA only appears when you bolt two physical CPU sockets together. Not any more. Modern AMD EPYC and Ryzen chips are built from several chiplets (CCDs) on one package, each with its own path to memory, stitched together by Infinity Fabric. Cross-chiplet accesses are measurably slower than same-chiplet ones, so a single processor can behave like a small NUMA machine — and the OS exposes it as multiple NUMA nodes. Apple's and Intel's big designs have similar internal distance effects. NUMA has quietly moved inside the chip.
A tempting mistake: "it's one address space, so every access costs the same." No. The shared address space
is a logical abstraction — any core can name any byte. But the physical distance to that
byte varies on a NUMA machine, and the hardware will not hide it from you. A load that hits local memory and
a load that crosses a socket link both look like plain