Lumiera
The new emerging NLE for GNU/Linux

Anyone reasoning about I/O, threading or real-time media processing eventually needs a feeling for how long things take. The trouble is that the relevant durations span more than twelve orders of magnitude — from a single CPU clock cycle up to the point where a human gives up waiting. Numbers on that scale resist intuition, and so we tend to reason about “fast” and “slow” without noticing that the two words can be separated by a factor of a million.

Caution Knowledge-Technology (»AI«) was used to structure and investigate this topic, starting from my own practical observations. Additional corroborating sources were identified and findings integrated into a coherent time axis view.
Note All values are order-of-magnitude figures for contemporary desktop and laptop x86 hardware. Expect a deviation of 2—3 x depending on microarchitecture, memory configuration, kernel version and load. The ratios are the point, not the digits.

The following table collects the figures that matter in this context. It is meant as a rough orientation, not as a benchmark result.

The last column in the table rescales everything by a single constant factor, chosen so that one CPU clock cycle (≙ 0.3 ns at 3.3 GHz) becomes one second of human time. Presenting this kind of human scale proportions is an effective way to convey what a cache miss or a disk seek actually costs, and what this means, assuming a machine’s point of view.

Overview Table

Figure Item Remarks Felt as (1 cycle ≙ 1 s)

Nanoseconds — inside the CPU

0.3 ns

one clock cycle

at 3.3 GHz; the machine’s own time quantum

1 second

~1 ns

L1 cache hit

about 4 cycles, fully pipelined

≈ 3 seconds

~1.5 ns

sfence

store fence, ≈ 5 cycles; rarely needed on x86

≈ 5 seconds

~2.7 ns

lfence

≈ 9 cycles; no ordering semantics on x86 beyond blocking speculation

≈ 9 seconds

~4 ns

L2 cache hit

≈ 12—15 cycles

≈ 13 seconds

~5 ns

lock add $0,(%rsp)

the actual smp_mb() on Linux; ≈ half the cost of mfence [lkml]

≈ 17 seconds

~5 ns

branch misprediction

pipeline refill

≈ 17 seconds

~10 ns

mfence / full barrier

≈ 23—33 cycles; comparable to a lock-prefixed RMW [downs]

≈ 33 seconds

~15 ns

L3 cache hit

shared, hence variable

≈ 50 seconds

~13 ns

DRAM, open row

a plain load that happens to hit an already-activated row [cxl]

≈ 43 seconds

~14 ns

DRAM, first access

device-level figure; essentially unchanged from DDR4 to DDR5 [ddr5b]

≈ 47 seconds

40…100 ns

contended cache line

core-to-core transfer; strongly topology dependent (same CCX / cross CCX / cross socket)

2—5 minutes

~100 ns

DRAM random access

full miss through the whole hierarchy; 122.9 ns measured by pointer chasing on DDR5 [cxl]

≈ 5 minutes

Nanoseconds to microseconds — entering the kernel

~25 ns

syscall via vDSO

not a real trap; almost as cheap as a function call [arkanis]

≈ 1.5 minutes

70…100 ns

syscall trap, bare

getpid() on a mitigation-free host [gms]

4—6 minutes

200…600 ns

syscall trap, mitigated

“a few hundred nanoseconds” on typical hosts with CPU-bug mitigations enabled [gms]

11—33 minutes

~1 µs

syscall doing real work

the round figure we use for the diagram

≈ 1 hour

~2 µs

context switch

scheduler plus cold cache on the new task

≈ 2 hours

Microseconds — the operating system

10…30 µs

thread wake-up

IRQ handling, try_to_wake_up(), scheduling onto a CPU [lkmlwake]

9 hours — 1 day

~31…57 µs

scheduling latency, avg

cyclictest at SCHED_FIFO 95 with memory locked [rhel]

1—2 days

~59…90 µs

scheduling latency, max

same run; the tail is what hurts [rhel]

2—3.5 days

50 µs

OS timer slack, default

the kernel may delay a timed wake-up by this much, deliberately, to coalesce wakeups [slack]

≈ 2 days

1 ns

OS timer slack, minimum

via prctl(PR_SET_TIMERSLACK) or /proc/PID/timerslack_ns [slack]

≈ 3 seconds

~10 µs

SSD read, best case

Optane-class (3D XPoint) at QD1 [intel]

≈ 8 hours

20…70 µs

NVMe SSD 4K random read

typical contemporary drive [simply]

1—3 days

50…100 µs

NVMe SSD, interrupt path

what you see without polling

2—4 days

100…200 µs

SATA SSD 4K random read

the interface costs, not just the medium [simply]

4—8 days

100 µs … 1 ms

deep C-state exit

wake from C6 and below; worse when many cores wake at once

4 days — 5 weeks

Milliseconds — where machine and human meet

5…10 ms

rotating disk, seek + rotation

7200 rpm; the number that has not improved in decades [simply]

6 months — 1 year

~10 ms

barely noticeable latency

the threshold below which a human stops perceiving delay

≈ 1 year

20 ms

one frame @ 50 fps

≈ 2 years

40 ms

one frame @ 25 fps

our actual deadline

≈ 4 years

~100 ms

UI starts to feel sluggish

≈ 11 years

Seconds and beyond — the human

~1 s

“the software takes time to respond”

acceptable, but noticed

≈ 1 century

~3 min

“time for another cup of coffee”

≈ 19 000 years

~15 min

“can’t wait, I’ll do something else”

≙ 1000 s, which conveniently closes the fourth decade block

≈ 95 000 years

The Memory Wall

For roughly two decades, CPU clock rates have hovered around 3 GHz while DRAM access latency has stayed near 60—100 ns. The consequence is arithmetic: a cache miss that cost a handful of cycles on early-1990s hardware now costs several hundred. The processor did not merely get faster than memory — it got faster than memory by a growing margin, and the entire cache hierarchy exists to paper over that gap. Note in the table above that the raw device-level latency of DDR5 is barely distinguishable from DDR4; the generational improvements went into bandwidth, not into latency.

Once several cores share one memory subsystem, a single uniform path to memory becomes the bottleneck. The answer was to move the memory controller onto the CPU die and attach memory per socket — and, with chiplet designs, effectively per die group. This is what NUMA (non-uniform memory access) describes: memory that “belongs” to the core you are running on is reached quickly, while memory belonging to a neighbour costs noticeably more. The practical upshot is that thread placement and memory placement are no longer independent concerns, and that a scheduler which migrates a worker between cores may silently convert local accesses into remote ones.

Cores also keep private caches, and a store performed on one core does not become visible to another core at the moment the instruction retires: it first lands in that core’s store buffer. Cache coherence guarantees that the cores eventually agree on the content of a cache line, but it says nothing about when, nor about the order in which several independent writes become visible. This is what memory fences (memory barriers) are for — they constrain the order, and thereby allow one core to reason about what an adjacent core has already done. The cost shows up in the table twice: an uncontended fence is roughly the price of an L3 hit, whereas a cache line actually bouncing between two cores costs about as much as going to DRAM. It should be noted that the expensive part is usually not the synchronisation primitive itself, but the cache coherence traffic it provokes.

And as far as memory synchronisation is concerned, there is another twist that has already led quite some programmers astray. Intel’s x86 implements a comparatively strong coherency model, Total Store Order (TSO): of the four possible reorderings, only StoreLoad is permitted. Acquire and release semantics are therefore free on x86 — they compile to plain MOV — and only sequential consistency requires an actual instruction. Quite to the contrary, ARM and POWER are weakly ordered and consequently offer a more differentiated set of barriers: ARM distinguishes DMB from the heavier DSB, and provides graded load acquire flavours such as LDAR and LDAPR; POWER separates the heavyweight sync from the cheaper lwsync. The trap for the unwary is that C++ memory_order distinctions which cost nothing on x86 have a real price on ARM — and, worse, that under-synchronised code can appear perfectly correct on x86 and fail on ARM.

Crossing into the Kernel

A system call is not a function call: it is a controlled trap. The syscall instruction switches the processor to privilege level 0, and the kernel entry path then swaps to the kernel GS base, switches stacks, and saves the user register state. Since the Meltdown/Spectre family of vulnerabilities, that sequence also includes a page-table switch (KPTI writes CR3, with the attendant TLB consequences) and one or more branch-prediction barriers — retpolines, IBPB or IBRS depending on hardware and configuration. On return, the whole dance runs in reverse.

The direct cost is measurable: a minimal syscall such as getpid() amounts to little more than two mode switches, and lands in the region of a hundred nanoseconds on a fast host without mitigations, rising to several hundred once mitigations are enabled [gms]. The indirect cost is harder to see and frequently larger. Kernel entry code and the syscall handler evict user-space data from L1 and L2, and disturb the branch predictors; the application resumes with a cold working set and pays for it in misses that no syscall benchmark will attribute to the syscall. A simple read() that happens to hit data already in the kernel cache can incur costs of around 1 µs.

The interesting question thus is is rarely “how expensive is one syscall” but rather “how many syscalls does this design require per unit of work” — which is the question io_uring was built to answer.

Operating System Slack

A thread that asks to sleep for a specific duration will not, in general, wake at that instant. Part of this is unavoidable: an interrupt must be handled, the sleeping task must be moved to the run queue, and the scheduler must actually place it on a CPU, which together account for something in the range of 10—30 µs [lkmlwake].

The larger part, however, is deliberate. Linux maintains a per-thread timer slack value, and the kernel is free to delay a timed wake-up by up to that amount in order to coalesce several wake-ups into one and thereby let the CPU stay in a low-power state longer. For every thread not running under a real-time scheduling policy, the default is 50 µs [slack]. This is not imprecision; it is a power-versus-precision trade that the kernel makes on the application’s behalf, and it can be adjusted — down to 1 ns — via prctl(PR_SET_TIMERSLACK) or the per-process /proc/PID/timerslack_ns interface.

The key distinction is therefore that between resolution and accuracy. Since Linux 2.6.21 the high-resolution timer subsystem reports nanosecond resolution, and clock_getres() will happily confirm it. The accuracy with which a nanosleep() actually returns is a different story altogether, two to three orders of magnitude coarser, and dominated by slack, wake-up latency and — if the core had gone idle — C-state exit time. Any design that assumes it can steer a worker thread with microsecond-scale sleeps should be checked against these figures first.

Sources

The in-text markers such as [downs] link down to the full citation here. Collected as a reading list, roughly in order of usefulness for further work.

downs

Travis Downs (6 July 2020) —  A Concurrency Cost Hierarchy

The best single entry point on why synchronisation costs what it costs.

See the discussion of "vanilla" versus "non-vanilla" instructions: on x86 plain accesses and non-atomic read-modify-writes are cheap while LOCK-prefixed RMWs are an order of magnitude slower, with MFENCE comparable to a locked instruction.

lkml

LKML thread (Nov 2015) —  [PATCH 3/4] x86,asm: Re-work smp_store_mb()

Kernel developers measuring MFENCE against XCHG and lock addq: Davidlohr Bueso measures XCHG against MFENCE on Ivy Bridge; Linus Torvalds reports that on Haswell lock addq costs pretty much exactly half of MFENCE, and explains that locked operations were the ones CPU designers had reason to optimise.

gms

Georg Sauthoff (30 Aug 2021) —  On the Costs of Syscalls

Careful cross-host microbenchmark of the mode-switch cost.

Median of 100 repetitions across a heterogeneous set of hosts; the conclusion is that user-kernel mode switches cost on the order of a few hundred nanoseconds on all hosts, with the fastest host switching modes in under 100 ns, and that the higher figures are explained by CPU-bug mitigations being enabled by default.

arkanis

Arkanis Development (5 Jan 2017) —  Measurements of system call performance and overhead

Compares a plain function call against getpid() issued via the syscall instruction and via the vDSO.

gregg

Brendan Gregg (9 Feb 2018) —  KPTI/KAISER Meltdown Initial Performance Regressions

Relates mitigation overhead to syscall rate, context switch rate and working set size — the systemic view rather than the per-call view.

lkmlwake

Michal Schmidt (17 Aug 2007) —  Reply in the LKML thread "nanosleep() accuracy"

The point being that a process always carries non-zero wake-up latency covering interrupt processing, waking and scheduling, and that 10—30 µs is not unreasonable.

rhel

Clark Williams (Red Hat) —  Finding Realtime Linux Kernel Latencies

Walks through a cyclictest run and how to read its output: sample cyclictest output and interpretation, section "Using cyclictest".

slack

Primary documentation —  man 7 time, section "High-resolution timers", and man 2 prctl, description of PR_SET_TIMERSLACK

The 50 µs default for non-realtime threads is stated explicitly in Metronome — adaptive and precise intermittent packet retrieval in DPDK (section II, discussion of nanosleep limits), and a practical encounter with the same default, including the /proc interface, is documented in OpenJDK issue JDK-8307766 .

cxl

arXiv:2503.22017 —  Performance Characterizations and Usage Guidelines of Samsung CXL Memory Module Hybrid Prototype

Table III contains a well-documented measurement of plain local DDR5: median latency 122.9 ns under Intel MLC pointer chasing versus 13.1 ns for a plain load; methodology (rdtsc, 10 000 repetitions, median) described in section III-C.

ddr5b

arXiv:2605.08725 —  Single 32-bit Sub-Channel DDR5 DIMMs

Useful for the point that DDR5 did not improve absolute latency over DDR4.

The relevant passage is in section "Latency Implications", noting that both DDR4 and DDR5 sit at approximately 14 ns first-access latency at their respective standard speeds.

simply

simplyblock glossary —  NVMe Latency

Compact summary of the NVMe / SATA SSD / rotating disk hierarchy: a modern NVMe SSD serves a random 4K read in roughly 20—70 µs, a SATA SSD in 100—200 µs, and a spinning disk in 5—10 ms.

intel

Intel —  Optane SSD DC P4800X Evaluation Guide

In Chapter 1.1, Page 5: “at queue depth of one, 99 out of 100 read operations … complete in single digit microseconds.”

Corroborated independently by Tom’s Hardware’s 2017 preview, which reports 10 µs latency for the same drive.

Note: Optane was discontinued by Intel in 2022; the figure is retained as the best-documented physical floor for this latency class.

A Note on Microbenchmarks

One caveat deserves a separate mention, as it explains why the published figures on synchronisation primitives differ so drastically from one another. A hot microbenchmark keeps every thread runnable and every core out of idle by its very construction. Under those conditions an uncontended atomic operation or a Mutex acquisition costs single-digit to low-three-digit nanoseconds, and a benchmark will faithfully report that. What such a benchmark structurally cannot observe is the cost of blocking — of a thread actually parking and later being woken — because pressing the benchmark is precisely what prevents that from happening.

The instruction-level cost of a synchronisation primitive is what a hot microbenchmark measures. Its systemic cost — coherence traffic under contention, and blocking and wake-up latency when it is not contended — is what production tail latency measures. The two values can differ by two to four orders of magnitude, and it is for this very reason that the table above covers both regimes.