Skip to content
Work Practice Products Coding interviews Machine learning coding Quant developer HFT course Blog Method Why me About Enquire Book a call

Quant developer HFT course · Module 8: Measuring latency

Finding memory bottlenecks

Lesson · 11 min

The question

Top-down analysis says the book update is back-end memory bound. What would you do next?

Memory bound means waiting for data

A memory-bound function spends its cycles waiting for loads to return from caches or main memory. The next questions are which loads, from which level, and whether the code is limited by latency (each load waits for the previous) or bandwidth (the memory system is delivering data as fast as it can).

Hot-path code in trading is almost always latency bound: it touches a few scattered cache lines per message, not gigabytes per second. Bandwidth limits show up in bulk work, such as replaying a day of data, recalculating risk across many positions, or copying large buffers.

Finding the loads

  • Miss counters by level (L1-dcache-load-misses, LLC-load-misses, and the more specific events like mem_load_retired.l3_miss on Intel) say how many loads went how far.
  • perf mem record samples individual loads with their data addresses and latencies, using precise sampling features of the processor. perf mem report then shows which instructions and which data addresses produced the slow loads, which usually points straight at one data structure.
  • perf c2c for loads that hit lines modified by other cores (Module 5).
  • Top-down's sub-levels split memory-bound time into L1, L2, L3 and DRAM bound, and into latency and bandwidth.

Latency bound: make fewer, closer, earlier loads

The fixes are the themes of Modules 1, 3 and 5:

  • Fewer lines per operation. Pack the fields the hot path reads together; shrink types; move cold fields out; replace node-based structures with flat ones.
  • Keep the working set resident. If the hot data fits in L2, misses become rare. Splitting a large table into a small hot part and a large cold part often does it.
  • Remove dependent chains. Index-based structures instead of pointer chains, so addresses can be computed in advance and misses overlap.
  • Prefetch where addresses are known ahead but the hardware cannot predict them.
  • Huge pages if TLB misses are a significant share.

Bandwidth bound: move fewer bytes

When the memory system is saturated, the fix is to move less data: smaller representations, structure of arrays so that a pass reads only the fields it needs, blocking loops so data is reused while in cache, avoiding writes of data nobody reads, and non-temporal stores for write-once output. Adding threads does not help a bandwidth-bound loop beyond the point of saturation, and may make it worse.

Telling the two apart

Measured bandwidth close to the machine's achievable peak (from a tool such as Intel's Memory Latency Checker or STREAM-like benchmarks) means bandwidth bound. Low bandwidth use with many long-latency loads means latency bound. Also telling: if two independent instances of the loop on different cores each run as fast as one alone, it was latency bound; if they slow each other, they are fighting over bandwidth (or a shared cache).

A worked example

The book update from the question: perf mem shows most slow loads in Book::apply reading Level objects, with addresses scattered across a large region. The levels are in a std::map, one node per level. The fix: keep levels near the touch in an array indexed by tick offset (Module 9), so the levels the feed updates most sit in a few contiguous, cache-resident lines. Verify: LLC misses per message fall, IPC rises, the stage timestamp for the book update falls at p50 and p99.

Check yourself

Question 1 of 3

Hot-path code in trading is usually limited by which memory property?

Question 2 of 3

What does perf mem add over the miss counters?

Question 3 of 3

Running two copies of a loop on different cores makes each one much slower. What does that suggest?

In the interview

Say your own answer out loud first, then open the one we would give.

How do you find out why a function is memory bound?

Confirm with top-down analysis and miss counters at each cache level, then use perf mem to sample slow loads with their instruction and data addresses, which points to the data structure. Check for cross-core contention with perf c2c. Then decide whether it is latency or bandwidth bound.

How do you tell latency-bound from bandwidth-bound code?

Compare the achieved memory bandwidth with what the machine can sustain: close to the peak means bandwidth bound; low bandwidth with many long-latency loads means latency bound. Running copies on several cores is another test: latency-bound code scales, bandwidth-bound code does not.

What fixes a latency-bound hot path?

Touching fewer cache lines per operation through compact layouts and hot-cold splitting, keeping the working set small enough to stay in cache, replacing pointer chains with index-based structures so misses can overlap, prefetching predictable indirect accesses, and huge pages if TLB misses matter.