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 5: The CPU

SIMD and vectorisation

Lesson · 11 min

The question

How could SIMD speed up finding a price level in a small array of prices, and why might the compiler not vectorise the obvious loop for you?

One instruction, many values

SIMD (single instruction, multiple data) registers hold several values side by side: 128 bits in SSE and NEON, 256 in AVX2, 512 in AVX-512. One instruction adds, compares or multiplies every lane at once. With 32-bit integers, an AVX2 compare tests eight values in one instruction.

px[0..7] eight 32-bit pricestarget broadcast to eight lanesone compare: eight results as a bit maskvpcmpeqd
SIMD: one instruction applies the same operation to every lane of a 256-bit register. Finding a price in an array becomes eight comparisons per instruction plus a scan of the resulting mask.

Finding a price in an array of a few dozen levels, a common operation in a book, becomes: broadcast the target into all lanes, compare eight prices at a time, turn the comparison result into a bit mask, and find the first set bit.

#include <immintrin.h>
int find_level(const std::int32_t* px, int n, std::int32_t target) {   // n a multiple of 8 here
    const __m256i t = _mm256_set1_epi32(target);
    for (int i = 0; i < n; i += 8) {
        __m256i v = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(px + i));
        int mask = _mm256_movemask_ps(_mm256_castsi256_ps(_mm256_cmpeq_epi32(v, t)));
        if (mask) return i + __builtin_ctz(mask);
    }
    return -1;
}

Auto-vectorisation

Compilers vectorise simple loops on their own at -O2 or -O3 when they can prove it is safe and profitable. Things that stop them:

  • Possible aliasing. In void add(float* out, const float* a, const float* b, int n), the compiler must allow for out overlapping a. It either adds a run-time overlap check or gives up. __restrict (a common extension) promises no overlap.
  • Early exits. A loop with break or return inside, like the search above, has an unknown trip count; GCC and Clang have limited support for vectorising these, so search loops often stay scalar.
  • Function calls that cannot be inlined.
  • Floating-point reductions, which need reassociation, as in the previous lesson.
  • Non-contiguous access such as walking a linked structure or gathering through indices; gathers exist but are slow.
  • Complicated control flow in the body.

-fopt-info-vec-missed (GCC) or -Rpass-missed=loop-vectorize (Clang) tell you why a loop was not vectorised. Reading those reports is more productive than guessing.

Layout for SIMD

SIMD wants the same field of many objects next to each other. An array of structs (Quote quotes[n]) puts each quote's price 24 bytes apart; a struct of arrays (int32_t px[n]; int32_t qty[n];) puts the prices contiguously, ready to load eight at a time. Hot paths that scan one field across many records often keep that field in its own array for this reason.

Choosing the instruction set

  • Compile for the production machine. -march=native on the build host, or the exact target such as -march=icelake-server, enables the instructions that machine has. A binary built for a newer CPU than it runs on crashes with an illegal instruction.
  • AVX-512 doubles the width again and adds masking that makes awkward loops easier to vectorise. On some older Intel server generations, heavy 512-bit instructions lowered the core's clock speed for a while afterwards, which could slow the scalar code around them; newer generations largely removed that penalty. It is a classic interview detail, worth stating with that qualification.

When SIMD matters on the hot path

The per-message path in a trading system is often short, branchy and dominated by memory access, which SIMD does not help. It pays off in specific places: parsing (finding delimiters, validating fields, converting digits), searching small arrays of levels or ids, checksums and decoding, and batch computations in pricing or risk. Use it where profiling shows such a loop, and keep a scalar version for testing and for machines without the instructions.

Check yourself

Question 1 of 3

Why might a compiler not vectorise for (i...) out[i] = a[i] + b[i]; in a function taking three float* parameters?

Question 2 of 3

Which layout lets a loop load eight prices with one 256-bit instruction?

Question 3 of 3

A binary compiled with -march=native on a new workstation is run on an older production server. What can happen?

In the interview

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

What is SIMD and where does it help in a trading system?

Instructions that apply one operation to several values packed in a wide register, eight 32-bit values in AVX2. It helps in loops over contiguous data: parsing and validating messages, searching small arrays of prices or ids, checksums, and batch numerical work. It does little for short, branchy, memory-bound per-message logic.

What prevents auto-vectorisation?

Possible aliasing between pointers, early exits from the loop, calls that cannot be inlined, floating-point reductions that would need reassociation, non-contiguous access, and complicated control flow. Compiler vectorisation reports say which one applied.

Any caveats with AVX-512?

On some older Intel server parts, sustained heavy 512-bit work reduced the core frequency for a while, which could slow surrounding scalar code; newer generations mostly fixed it. And the binary must be built for CPUs that actually have the instructions, with a fallback if it must run elsewhere.