39

Mechanical sympathy

#
  • heuristic
  • context-dependent
  • fragment

Scope. All examples assume a 64 B cache line — that is platform-specific. Check the size on the target platform, and the gain with hardware counters rather than reasoning. @Contended is an internal JDK API and needs a separate launch flag.

Conflicts with: Boring technology ↔ mechanical sympathy

Primary sources: Soares, Stumm — FlexSC: Flexible System Call Scheduling with Exception-Less System Calls (OSDI 2010)

References: Linux kernel documentation — Page Table Isolation (PTI)

Cache line 64B, false sharing, NUMA, syscall cost. Adjacent to DOD, but broader — it includes knowing the runtime.

One key unlocks the whole list: the processor works not with bytes but with 64-byte cache lines. False sharing: two threads write to different variables that happen to land on the same line — logically there is no contention, physically the line ping-pongs between cores, and the code slows down without a single lock (the fix is padding to the line size). NUMA: on a multi-socket machine memory has a “local” and a “remote” socket, and reaching the remote one costs more, so where a thread runs relative to its data matters. A syscall is not a switch to another task and does not flush the CPU caches, but it is not free either. The direct cost is the privilege transition and the kernel's own work, plus mitigations: under KPTI every entry and exit also reloads the page tables (CR3), about a hundred cycles by the kernel's own documentation, and on CPUs without PCID each such write flushes the whole TLB. The indirect cost is that kernel code fills the caches, the TLB and the branch predictor with its own state, and user code runs slower until it warms them back up: Soares and Stumm (FlexSC, OSDI 2010) measured up to 14,000 cycles of lowered throughput after a single pwrite and a 20–60% drop in user-mode instructions per cycle at server-like syscall rates. A blocking call may also hand the core to another thread. Hence batching instead of one call per item. What all four share: the cost is invisible in the source and shows up only when measured (37).

Code example (fragment)
fragment
Java
// false sharing: two counters on one cache line kill a multithreaded increment
@jdk.internal.vm.annotation.Contended  // or pad manually
volatile long producerIdx;
Rust
#[repr(align(64))]                       // a counter on its own cache line
struct PaddedCounter(AtomicU64);
Go
// Idiom: per-P sharded counters + padding
type shard struct { n atomic.Uint64; _ [56]byte } // pad to 64B
Python
# In CPython the cost is dominated not by cache lines but by the interpreter loop,
# so mechanical sympathy here means moving the loop out of Python into C
total = sum(row.price for row in rows)        # ~10**6 interpreter steps

prices = np.frombuffer(buf, dtype=np.int64)   # the same computation in one call:
total = int(prices.sum())                     # the loop and vectorization run in C

# without dependencies, memoryview does the same:
# slicing bytes copies; slicing a memoryview does not
window = memoryview(buf)[offset:offset + size]

Last technical review: 2026-08-04 · Reference · 112 principles