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.
alignas(32) data — and name the three things that make the vectorizer give up.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
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.
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.
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.
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.
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.
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.
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.
-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×.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.
// 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]; }
// 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; }
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.
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.
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.
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.
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.
Baseline: a stock kernel, a shared core, 4 KB pages. Every number below is measured against this.
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.
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.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.
Every one of these has been submitted, graded, and argued about. Open them.
-ffast-math and calling it a winIt 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.
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.
_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.
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.
-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.
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?
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.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.-march=native bought you,
then throw it away.alignas(64) and __builtin_prefetch, and tune the prefetch
distance until the counters agree with you.-O0, -O2,
-O3, -O3 -march=native, + -funroll-loops,
+ -flto — with the checksum unchanged in every row.-O0, one sentence of cause each.-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.on_book hot loop, pin its thread, cut
hot-path syscalls — then report p50/p99/p99.9 before and after on the same tape.starters/hw13/kernel.cpp is the
frozen kernel; scripts/latency_replay.py and scripts/latency_report.py
are the graded instrument.