Diagnosing LLVM Loop Vectorization Failures via the Cost Model
Learn why LLVM refuses to vectorize your loops and how to read cost-model diagnostics, compare LoopVectorize with Polly, and apply targeted fixes.
09 Jul 2025, 03:29 UTC

You write a loop that looks perfect for SIMD (Single Instruction, Multiple Data) execution, but LLVM keeps emitting scalar assembly. This is rarely a bug in the code generator. It is usually a deliberate decision by the cost model: the compiler estimates that vectorizing the loop—paying for shuffles, gathers, alignment handling, and tail epilogues—costs more cycles than it saves.
The fix is not to blindly add pragmas. It is to read the compiler's reasoning, identify which factor inflated the estimated cost, and remove that factor. This guide walks through that diagnostic loop. Examples assume LLVM 15 or later; note that LLVM 15 revised the cost model for non-unit-stride loops, so decisions can shift between releases. Always re-verify after a toolchain upgrade.
Recognizable condition
You compile with -O2 or -O3 and a target that supports SIMD (for example -march=x86-64-v3 or -mcpu=native), but the generated assembly contains no packed vector instructions such as vmulps/vaddps on AVX-capable x86, or the loop runs at scalar speed under profiling. Alternatively, vectorization worked in one LLVM version and silently stopped after an upgrade—a known hazard, since cost-model changes between releases can alter decisions without any source change.
Common causes at a glance
| Factor | Effect on the decision | Typical remark |
|---|---|---|
| Low or unknown trip count | Setup and epilogue cost exceeds savings for short loops | "trip count is too small" or unknown bounds |
| Loop-carried dependency | Iteration N reads a value written by iteration N-1; vectorization is illegal, not just unprofitable | "loop carried dependence" from the dependence analysis |
| Non-unit stride | Accesses like a[i*2] need gather/shuffle, raising cost; heuristics changed in LLVM 15 | cost report lists expensive interleaved/gather access |
| Unknown alignment / aliasing | Compiler must assume unaligned loads and emit runtime alias checks | "cannot prove no aliasing" or added runtime checks |
| Calls or control flow in body | Function calls and if/break may block or require expensive predication | "call instruction cannot be vectorized" |
Step 1: Read the compiler's reasoning
Run these from a shell with your normal compile flags. No special permissions are needed; the commands only read your source and write report text to stdout or a file.
The most direct tool is optimization remarks. Compile with:
clang -O3 -march=native -Rpass=loop-vectorize -Rpass-missed=loop-vectorize -Rpass-analysis=loop-vectorize main.c -o main
-Rpass-missed=loop-vectorize prints one line per loop that was rejected, with the reason. -Rpass-analysis=loop-vectorize adds the cost detail: the estimated scalar cost, the vector cost at each candidate vectorization factor, and the factor chosen (or why none was profitable). Costs are in abstract "cost units" that roughly track reciprocal throughput on the target; treat them as relative, not cycles.
For a deeper look, run the pass standalone on LLVM IR:
clang -O3 -march=native -emit-llvm -S main.c -o main.ll
opt -passes=loop-vectorize -force-vector-width=4 -S main.ll -o main.vec.ll
Here -force-vector-width=4 bypasses the profitability check and forces a vector factor of 4. This is a diagnostic, not a fix: if the forced version is slower, the cost model was right. Placeholders: replace main.c and the width with your file and your target's register width (4 for 128-bit float ops, 8 for 256-bit).
Step 2: Verify with real measurement
Cost models are heuristics. Before and after any change, confirm with measurement:
- Inspect the IR (
main.vec.ll) for vector instructions such as<4 x float>types andfadd <4 x float>. - Feed the generated assembly to
llvm-mca(LLVM Machine Code Analyzer) to estimate per-iteration throughput on a specific CPU model, e.g.llvm-mca -mcpu=skylake-avx512 main.s. Compare the scalar and vector versions rather than trusting the cost units. - Benchmark the actual binary with fixed inputs and multiple runs; the cost model cannot see cache behavior.
Step 3: Compare LoopVectorize against Polly
LLVM ships two relevant optimizers: the standard LoopVectorize pass and Polly, a polyhedral loop optimizer that can restructure loop nests before vectorization. Polly is sometimes more aggressive on multi-dimensional loops but can noticeably increase compile time, so enabling it build-wide is a real engineering trade-off.
To test whether Polly changes the outcome on a specific file:
clang -O3 -mllvm -polly -mllvm -polly-vectorizer=stripmine main.c -o main_polly
If Polly vectorizes a loop that LoopVectorize rejected, the blocker was likely a heuristic or transformation limitation in the standard pass rather than a true dependence. Use this as evidence when deciding whether to restructure the loop or file an upstream issue. Do not leave -mllvm -polly enabled in production builds without measuring compile-time impact and validating numerical results.
Fixes tied to findings
Finding: aliasing or alignment unknowns
Add restrict qualifiers (C) or __restrict__ to pointer parameters so the compiler can skip runtime alias checks. Align buffers to the vector width and tell the compiler with __builtin_assume_aligned. These changes reduce both legality blockers and estimated cost.
Finding: non-unit stride cost
Restructure data from Array of Structures to Structure of Arrays so inner-loop accesses become contiguous. For example, split struct Point { float x, y; } pts[N] into separate xs[N] and ys[N] arrays. Contiguous loads are dramatically cheaper in the cost model than gathers.
Finding: small trip count
If the loop really is short, scalar code may be correct—verify with llvm-mca before forcing anything. If the trip count is large but unknown at compile time, a #pragma clang loop vectorize(enable) on that specific loop tells the compiler to vectorize when legal, overriding only the profitability heuristic. Prefer this over global force flags, and re-benchmark to confirm the win.
Finding: true loop-carried dependence
No pragma makes an illegal transformation legal. Restructure the algorithm: separate the reduction (which LLVM can vectorize with -ffast-math for floating point, at the cost of reassociation) from the sequential recurrence, or compute the recurrence in closed form.
Escalation criteria
Escalate to an upstream LLVM issue when: (a) -force-vector-width produces correct, measurably faster code, proving the cost model misjudged a legal loop; (b) behavior regressed between releases with identical flags, which you can isolate by diffing -Rpass-analysis output across versions; or (c) Polly succeeds where LoopVectorize fails on a simple loop. Include the reduced IR, target triple, flags, and both cost reports. If none of these apply and measurement shows the scalar code is genuinely competitive, accept the compiler's decision—the model did its job.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.