WEEK 13 · 15-WEEK TRACK · SESSION 8 · 9-SESSION TRACK

Low-Latency Design — SIMD & Kernel Bypass

Week 12 made the wire lean. This week the box itself is the bottleneck: 64-byte cache lines, eight lanes per instruction, a branch predictor you can help or fight, and a kernel network stack you can walk around entirely. Every item here is a measured microsecond, not a style preference.

Lab: starters/hw13/kernel.cpp — flags only, ~6.5× faster, identical checksum HW13: flag matrix + perf stat + one line on -ffast-math Prove it: p99.9 before/after on the same replayed tape
Part 1

1What is it? Winning back the last microseconds, in three places

By week 13 your algorithm is right and your allocations are gone. What is left is hardware: the memory you touch, the instructions you issue, and the kernel you ask for favours. Those are the three levers, and each one is worth a different number of microseconds — so the order you pull them in matters.

Memory: the wall

An L1 hit is ~4 cycles (~1 ns). A miss to DRAM is 200+ cycles (~80–100 ns). Memory moves in 64-byte lines, never in bytes: touch one field and you pay for sixty-four. On the hot path you are almost never compute-bound — you are waiting.

Instructions: the lanes

One AVX2 register is 256 bits — eight 32-bit floats per instruction; AVX-512 doubles that again. -O3 -march=native will do it for you on a clean loop and refuse on a dirty one. Knowing which is which is the skill.

The kernel: the tax

A syscall is a mode switch — hundreds of ns, sometimes µs under load, plus a polluted cache. The kernel network stack copies, checks and routes every frame. Kernel bypass maps the NIC into user space so it never happens.

The order to pull them in. Layout first (it is free and it never regresses), then vectorization (cheap, sometimes just a flag), then the kernel and the OS (a platform change), and only then colocation — because colo buys wire time and nothing else. A 200 µs software hiccup dwarfs any wire saving, which is why week 14 exists.

Interactive · array-of-structs vs struct-of-arrays

You are scanning one field of every order — say the price, to recompute an imbalance. Pick a layout and watch how many 64-byte cache lines the scan drags in, and how much of each line you actually use. The grid shows one box per line; violet is useful bytes, grey is bytes you paid for and threw away.

bytes the scan uses bytes fetched and wasted line never touched

The same struct, both ways

Part 2

2How it works — lanes, branches and the line you fetch early

Three mechanisms, and they interact. SIMD gives you width; a data-dependent branch takes it straight back; a prefetch hides the latency that neither of them can do anything about. Play with all three before you write an intrinsic by hand.

Interactive · the vector lanes

Step or run the reduction over a price array. Each frame consumes one instruction's worth of data — one lane if you are scalar, eight if you are on AVX2. Then change the loop shape and watch the vectorizer refuse.

—

What the compiler emits

Check before you hand-write. Build with -O3 -march=native -fopt-info-vec and read the loop vectorized lines. If the compiler already did it, an intrinsic buys you nothing and costs you portability — the HW13 lab makes exactly that point with flags only, no source edits, and still lands near 6.5×.

Interactive · what a mispredicted branch costs

A correctly predicted branch is nearly free. A miss flushes the pipeline — roughly 15–20 cycles, about 5 ns at 3.4 GHz. Branchless code pays a small fixed cost on every iteration and never flushes. Move the predictability slider and find the crossover.

Branchy

// one unpredictable branch per level
for (std::size_t i = 0; i < n; ++i) {
  if (px[i] > ref) buy += qty[i];
  else            sell += qty[i];
}

Branchless

// same answer, no branch to mispredict
for (std::size_t i = 0; i < n; ++i) {
  const auto m = -static_cast<std::int64_t>(px[i] > ref);
  buy  += qty[i] &  m;   // mask is all-ones or 0
  sell += qty[i] & ~m;
}

Prefetch — hide what you cannot avoid

The hardware prefetcher follows regular strides on its own. It cannot see a data-dependent address, which is exactly the case in an order-book walk. A software hint costs nothing if it is wrong, only bandwidth:

__builtin_prefetch(&level[i + 4], 0, 3);  // read, keep it: L1
__builtin_prefetch(&tape[i + 16], 0, 0); // read, streaming: don't retain

Distance is the whole knob. Too near and the line has not arrived; too far and it is evicted before you use it. Tune it with perf stat -e L1-dcache-load-misses,LLC-load-misses — never by feel.

The week-13 kernel, for real

This compiles as written under clang++ -std=c++20 -O2 on any target: the AVX2 path is guarded, the scalar path is the reference, and both must produce the same number — which is the checksum rule from the lab.

#include <cstddef>
#if defined(__AVX2__)
#include <immintrin.h>
#endif

// Weighted order-book imbalance. obi and w are alignas(32); n % 8 == 0.
float weighted_obi(const float* obi, const float* w, std::size_t n) {
#if defined(__AVX2__)
  __m256 acc = _mm256_setzero_ps();
  for (std::size_t i = 0; i < n; i += 8) {
    __builtin_prefetch(&obi[i + 16], 0, 0);   // the next 64 B line
    __m256 a = _mm256_load_ps(&obi[i]);         // aligned: needs alignas(32)
    __m256 b = _mm256_load_ps(&w[i]);
    acc = _mm256_fmadd_ps(a, b, acc);            // a*b + acc, 8 lanes, 1 instr
  }
  alignas(32) float t[8];
  _mm256_store_ps(t, acc);
  return t[0]+t[1]+t[2]+t[3]+t[4]+t[5]+t[6]+t[7];   // horizontal reduce
#else
  float s = 0.0f;                                 // portable reference path
  for (std::size_t i = 0; i < n; ++i) s += obi[i] * w[i];
  return s;
#endif
}
// release: -O3 -march=native -flto -DNDEBUG -g

Two things to notice. The #if is not defensive clutter — it is what lets one repo build on the grader, on your laptop and on an ARM box, and it gives you a scalar oracle to diff the vector result against. And the horizontal reduce at the end is not free: it is why vectorizing a very short loop can be slower than not bothering.

Part 3

3Why it matters — the kernel is the biggest single line item

All the SIMD in the world moves a few hundred nanoseconds. The path a packet takes from the wire to your on_book moves microseconds. That is why kernel bypass and core isolation come after the cheap wins but before the expensive ones: they are the largest block on the chart, and the arena's Latency Board reads them off your p99.9.

Interactive · the packet path, stage by stage

Same frame, same NIC, three different paths into your process. Bars are microseconds; the numbers are representative of a modern server NIC under light load and are labelled estimates, not guarantees — the shape is the lesson.

Kernel stack · blocking socket

Stage detail — click a bar

How you get there

Interactive · the tuning checklist

Eight platform changes, each with an estimated effect on p50 and on p99.9, and each with a cost. Tick them and read the running total — then read the warnings, because several of these are worthless or actively harmful on their own.

Nothing ticked

Baseline: a stock kernel, a shared core, 4 KB pages. Every number below is measured against this.

Warnings

    The resulting boot line and startup

    What the arena's Latency Board actually measures

    The instrument

    The exchange stamps your reaction with a monotonic clock in microseconds: from the book_snapshot you were sent to the order you sent back from on_book. Every tick is a sample. The board reports p50 · p99 · p99.9 plus the sample count per participant, and it sorts by the tail, not by the median — races are won on p99.9.

    A p99.9 needs on the order of a thousand samples before it means anything. Short sessions give you a noisy tail; that is a property of the statistic, not of your code, and saying so out loud is part of a correct report.

    Offline is where you optimize. scripts/latency_replay.py drives your binary over a deterministic tape and scripts/latency_report.py prints the same percentiles with a tail histogram. Same tape, one change at a time — otherwise you cannot attribute the delta to anything.

    Code versus colocation

    The venue adds an outbound delay per order. The upgrade shop moves you from the default tier to the colocated tier — a straight cut in wire time you buy with cash rather than earn with code. Both move the same percentiles, which is what makes the comparison honest and the decision interesting.

    • Colo shifts p50 and the tail together, by a fixed amount. It is worth most to latency-arbitrage and queue races (week 15).
    • Code fixes shift the tail specifically — that is what SIMD, no-alloc and pinning do.
    • If your p99.9 is milliseconds and the wire tier is tens of µs, colo is a waste of money until week 14 is done. Fix the software tail first.
    Week 13 grading, in the same currency. A correct vectorized hot loop that matches the scalar result; a measured p99.9 improvement on the same tape; a prefetch and a pinned thread with the reasoning; and the colocation economics argued with numbers.
    Part 4

    4Five ways this goes wrong

    Every one of these has been submitted, graded, and argued about. Open them.

    Turning on -ffast-math and calling it a win

    It is faster. It also permits the compiler to reassociate floating-point arithmetic, so a reduction that summed left-to-right now sums in vector lanes and rounds differently. In the HW13 kernel that shows up immediately: the time drops and the checksum changes. A build that computes something else is not an optimization.

    The same flag enables assumptions about NaN and infinity that a pricing kernel has no business making — a single bad tick that used to produce a NaN you could see now produces a plausible number you cannot.

    Fix: treat the checksum as the contract. If a flag changes it, the flag is out — or it goes in with a written argument about the tolerance and where it is safe. Reassociation is fine in a statistic you sample; it is not fine in a price you send.

    Hand-writing intrinsics the compiler was already emitting

    Intrinsics are unportable, harder to read, and easy to get subtly wrong — and on a clean contiguous loop -O3 -march=native produces the same instructions for free. Writing them by hand first means you spent a day to match the baseline.

    The signal is one flag away: -fopt-info-vec tells you every loop the vectorizer took. If your hot loop is in that list, the answer to "should I write an intrinsic" is no.

    Fix: measure, then read -fopt-info-vec, then hand-vectorize only the loops the compiler refused — and when you do, keep the scalar version compiled in as the oracle you diff against.

    An aligned load on unaligned data

    _mm256_load_ps requires a 32-byte-aligned address. Give it anything else and you get a fault — or, worse, you get away with it on your laptop and fault on the grader. Data from new, from the middle of a struct, or from an offset into a buffer is not aligned because you hoped it was.

    The mirror mistake is using _mm256_loadu_ps everywhere and assuming it is free. It is nearly free when the data happens to be aligned and it is not when a load straddles two cache lines — you pay for both.

    Fix: alignas(32) on the buffer (or alignas(64) to own a whole line), assert the alignment once at startup, and keep the loop count a multiple of the lane count so there is no scalar tail to get wrong.

    Busy-polling a core you do not own

    Spinning is the right answer only when the core is yours. Spin on a core the scheduler is also using and you have built the worst of both worlds: 100% CPU, the other tenant still preempts you, and your p99.9 gets worse than the blocking version you replaced.

    The same applies to a laptop with four cores and a browser open. Pinning is not a flag you sprinkle on; it is a claim about the machine that has to be true.

    Fix: pin with pthread_setaffinity_np, hand the core over with isolcpus plus nohz_full and rcu_nocbs, move IRQs off it, and keep the poller and the strategy on different isolated cores so they never contend. Then measure — if p99.9 did not improve, you did not actually get the core.

    Building with -march=native somewhere else

    -march=native means "this exact CPU". Build on a machine with AVX-512 and run on one without, and you do not get a slow binary — you get SIGILL on the first vector instruction, usually inside a hot loop, usually the first time it matters.

    It also silently changes what a portable-looking #if defined(__AVX2__) block compiles to, so the code you tested is not the code you shipped.

    Fix: build on the target microarchitecture, or name the baseline explicitly (-mavx2 -mfma) and dispatch at runtime if you truly need one binary for several machines. Either way, record the flags next to the latency numbers — a percentile without its build line is not a measurement.

    Part 5

    5Check yourself

    Three questions, instant feedback, nothing recorded.

    Q1You scan the 8-byte price of 64 orders. In AoS the order struct is 32 bytes; in SoA the prices are contiguous. How many 64-byte cache lines does each scan touch?

    Q2Your hot loop has an if (px[i] > ref) inside it and is fully memory-resident. You add -march=native. What do you expect?

    Q3Your p99.9 is 1.8 ms. The colocated tier would cut roughly 180 µs of wire delay per order and costs real budget. Buy colo, or hand-vectorize the imbalance loop?

    Part 6

    6Do the work

    In the lab (~45–60 min)

    • Build starters/hw13/kernel.cpp at -O0, then at -O3 -march=native -funroll-loops. Expect roughly 6.5× and an identical checksum. You never touch the source.
    • Explain the win with perf stat -d: fewer retired instructions, higher IPC, and what happened to branch-misses. No perf? -fopt-info-vec plus the time_ms delta stands in.
    • Write the AVX reduction by hand once, so you know what -march=native bought you, then throw it away.
    • Demo alignas(64) and __builtin_prefetch, and tune the prefetch distance until the counters agree with you.

    Homework 13 & the weekly exercise

    • HW13 (flags only): a time matrix over -O0, -O2, -O3, -O3 -march=native, + -funroll-loops, + -flto — with the checksum unchanged in every row.
    • HW13: for your best build, quote the drop in instructions and in branch-misses versus -O0, one sentence of cause each.
    • HW13: try -ffast-math. Does the checksum still match? One line on why it is not free, and whether you would ship it in a pricing kernel.
    • In your bot: vectorize and align the on_book hot loop, pin its thread, cut hot-path syscalls — then report p50/p99/p99.9 before and after on the same tape.
    • Colocation economics: your p99.9 at the default tier versus the colocated tier, and whether the edge is worth the cost. Argue it with numbers.
    Code
    Get the code
    Both tracks share one starter tree. starters/hw13/kernel.cpp is the frozen kernel; scripts/latency_replay.py and scripts/latency_report.py are the graded instrument.
    Previous
    ← Week 12 · Async I/O & Serialization
    The wire got lean. This week the machine underneath it did.
    Next
    Week 14 · Profiling, the Tail & Production →
    You have the levers. Now find out which one your tail is actually waiting on — and prove the fix with a histogram.