How do you know the compiler actually vectorized your loop?
My options pricer's build log said "loop vectorized", but the pricing loop was still scalar. The success message belonged to a trivial loop at the end of the benchmark. To check the loop that matters, enable both successful and missed-vectorization reports on your optimized build, then match each message to its source file and line.
Compiler flags for vectorization reports#
| Compiler | Report successful vectorization | Report misses and reasons |
|---|---|---|
| MSVC | /Qvec-report:1 |
/Qvec-report:2 includes successes and misses with reason codes. |
| GCC | -fopt-info-vec-optimized |
-fopt-info-vec-missed reports missed vectorization opportunities. |
| Clang | -Rpass=loop-vectorize |
-Rpass-missed=loop-vectorize reports misses; -Rpass-analysis=loop-vectorize identifies the statements responsible. |
These are reporting options for an optimized build. They do not force the compiler to vectorize a loop. The flags are documented by Microsoft, GCC, and LLVM. MSVC is the only one I ran on this engine. After locating the right loop in the report, measure it on one core and check the results against the scalar implementation.
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 needed the same separation. The jump from fifteen million to 114 million came from a better polynomial and more cores, with no SIMD in it. Four lanes describe how many values a vector operation handles; they do not establish how much faster the whole loop will run.
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. One core gained about four times, and across all cores the engine reached about 215 million a second. The rewrite changed both the math implementation and the execution width, so those timings do not isolate the gain from SIMD alone.
Three checks that catch a claimed win#
Ask the compiler for its misses using the flags above. Match each message 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.
Measure the loop on one core before measuring all of them. If the gain is smaller than expected, first confirm that the reported loop does the work you timed. Then investigate memory traffic, scalar work left in the loop, and overhead on short loops. A smaller gain can have several causes; a larger gain can include changes beyond SIMD. Keep the input, thread count, and math implementation comparable when trying to attribute the difference.
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:
- Pricing 215 million options a second with SIMD C++, the full climb from fifteen million to 215 million, including the memory-layout guess that came before this one
- How to backtest without fooling yourself, the same measure-before-believing discipline pointed at signals instead of speed
- Lesson 3: Enforce Project Rules with Automated Checks, designing checks that reject a known failure
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?
- Four SIMD lanes do not guarantee a fourfold speedup. My pricing loop gained about four times on one core after I replaced the scalar exponential and logarithm with hand-written four-wide versions. That changed both the math implementation and the execution width, so the result does not isolate SIMD's contribution. Across all cores the engine went from about 114 million to about 215 million options a second.
- 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.