← Back to Blog

How do you know the compiler actually vectorized your loop?

About this artifact

essayon-the-recordmaintained

markets · since 2026

Hobbyist research, not investment advice.

The vectorization report is a per-loop verdict, and my options pricer sat at about 114 million options a second while its build log said "loop vectorized", because the loop the compiler had vectorized was a trivial one at the end of the benchmark. The pricing loop, the one full of exp and log calls, stayed scalar the whole time, with its failure notice sitting in a report I had not opened. The engine reached about 215 million only after I wrote the vector math by hand.

A benchmark with two loops beside a compiler report with two lines: the green "loop vectorized" line points at the trivial trailing loop, and the red "loop not vectorized" line, never asked for, points at the pricing loop that calls exp and log.

The report was true about the wrong loop#

The pricer is the Black-Scholes engine from the 215 million post. A clean first version priced about fifteen million options a second on one core. Swapping one modern library call for a sixty-year-old polynomial and threading the embarrassingly parallel loop took it to about 114 million, and none of that came from SIMD. The plan had always assumed the compiler would vectorize the clean inner loop on its own, because that is what the guides say, and because the build log agreed.

My compiler, MSVC, prints a line for each loop it vectorizes when asked, and my build log had one: a trivial loop at the end of the benchmark. The loop that priced options calls exp and log, and my compiler would not fold a loop full of library calls into vector form. Some toolchains will; mine did not. Its failure notice for the pricing loop sat in a report I had not opened, so the loop that mattered stayed scalar on every build while the log kept reporting a win.

The numbers carried the same tell. A four-wide vectorization can give, by itself, at most about four times on one core, and this loop, once the SIMD was real, got close to that. The jump from fifteen million to 114 million came from a better polynomial and more cores, with no SIMD in it, which is a fine speedup and a different one.

Test each vector function before wiring it in#

I wrote the exponential and the logarithm four wide, from classic polynomial cores, intrinsics all the way down. Before wiring either into the pricer I tested each one in isolation against the standard library, the exponential against the exponential rather than the pricer against the pricer. When a bug showed up, the isolated test named the function instead of leaving me to chase a wrong price three layers downstream. With real SIMD underneath, one core gained a clean four times, right at the theoretical width, and across all cores the engine reached about 215 million a second.

Three checks that catch a claimed win#

Ask the compiler for its misses. MSVC's /Qvec-report:1 prints only the loops it vectorized; /Qvec-report:2 adds the loops it declined, with a reason code. GCC's -fopt-info-vec-missed lists the missed loops, and Clang names them under -Rpass-missed=loop-vectorize and explains them under -Rpass-analysis=loop-vectorize. Then match each line to a source line and find the loop you care about. A report with only successes in it says nothing about the loop that is missing from it.

Read the width in the numbers. Vectorizing four at a time cannot, on its own, give much more than four times on one core, and a single-core gain well short of the width after a "loop vectorized" message usually has one of two causes: the loop that got vectorized was not the one doing the work, or the loop is waiting on memory rather than arithmetic. Measure one core before you measure all of them; threading hides the width.

Test the vector functions against the scalar library, one function at a time. This says nothing about what the compiler did. It checks the code you wrote to replace the compiler, over the inputs and tolerances you chose, which is the check you need the moment you stop trusting the report.

Treat the published timings as a shape#

The engine is not public and I have not re-run it for this post, so every number here is the one I published in July, and desktop timings wander ten or twenty percent run to run. Read fifteen, 114 and 215 as a shape. It was built to learn from, and none of it is a trading system. The compiler flags above are the general ones from each toolchain's documentation; MSVC's is the only one I ran on this code.

If I built the pricer again, the benchmark would carry the check itself: single-core throughput of the vector build against the scalar build on the same machine, with a floor set from repeated runs, so that a build where the width has gone missing fails on the spot instead of shipping behind a success line in the log.


Related:

Questions this post answers

How do I check whether my compiler vectorized a loop?
Ask the compiler for the loops it declined, then match each message to the source line of the loop that does the work. MSVC prints declined loops with a reason code under /Qvec-report:2; GCC under -fopt-info-vec-missed; Clang names them under -Rpass-missed=loop-vectorize and gives the reason under -Rpass-analysis=loop-vectorize. A success message on its own only proves that some loop was vectorized.
Why does the compiler refuse to vectorize a loop that calls exp or log?
Calls into the math library commonly block vectorization, and the compiler's missed-optimization report records the reason it gives for each loop. Whether a given toolchain can vectorize a loop with exp and log in it depends on the toolchain and its settings. Some toolchains will; mine did not, and the reason code for my loop is not in the published record.
How much speedup should real SIMD give?
From the vectorization alone, about the vector width on one core at best: four times for a four-wide vector. My pricing loop gained a clean four times on one core once the exponential and logarithm were hand-written four wide, and the engine went from about 114 million to about 215 million options a second across all cores.
Can I reproduce the 215 million options a second number?
No. The engine is not public, and the timings, published in July 2026, wander ten to twenty percent run to run on a desktop, so treat them as a shape. The report check and the width check in the post apply to any loop.

Keep reading

Post

Pricing 215 million options a second with SIMD C++

I wrote a C++ options pricer to learn low-latency numerics. The first clean version priced fifteen million options a second; getting to 215 million was less about clever code and more about being wrong, in public with myself, about where the speed would come from.

Read
Post

How to backtest without fooling yourself

A backtest's job is not to find an edge. It is to stop you from believing in one that is not there. The toolkit I used to test my own trading engine, and the part where it killed my single best signal.

Read
Post

Lesson 3: The check that fails

Third lesson in a series on running an AI-powered software team of one. A rule you write in a README gets broken by the first session that never reads it. A folder of one-script checks, run by git before every push, turns your most-repeated rule and the verify line from Lesson 1 into commands that fail. Your agent writes each check; you watch it fail once.

Read
Post

What Newey-West does to a backtest t-statistic: 18 months became two

A Newey-West standard error corrects a t-statistic for autocorrelation. On my engine's 12-month panel it turned 18 monthly readings with a t-statistic of negative 18.8 into about two independent observations. The arithmetic, the lag choice, and the gate I built so it cannot be skipped.

Read
Post

How many AI coding agents can you actually run at once?

About ten on one subscription, and the model is never what stops you. Four shared resources under the agents set the ceiling: the link to the vendor and its quota window, the git checkout, the port your tests bind, and the file every branch regenerates. What a seven-agent wave broke, with counts.

Read
Post

How to make an LLM say I don't know, and how to measure that it did

Ballast declines 24 of its 89 golden questions on purpose and answers the other 65 with citations. As of the August 16 ledger the answer-or-decline call was right on all 89, and one of the 65 answers was unfaithful to its sources. Where the refusal comes from, how the eval scores it, and why 24 is the number to read, not 100 percent.

Read

Follow the work

New tools and writing as they ship — pick a channel.

Written by Eric Caskey. I build AI tools you can actually use. Explore the Tools or see the case studies.