Part 5 · SIMD and the modern ISA
Why autovectorisation usually fails
The compiler wants to vectorise your loop. It is usually blocked, always for a nameable reason, and it will tell you which one if you ask.
What this lesson is trying to break (2)
- The compiler vectorises anything simple.
- If it did not vectorise, the compiler is not smart enough.
The compiler is trying. Vectorisation is enabled at -O2 in modern GCC and Clang
and it will take it whenever it can prove the transformation legal. The interesting
question is what stops it, and the answers are few and specific.
Ask, do not guess
Before anything else: the compiler will tell you.
-fopt-info-vec-missed # GCC
-Rpass-missed=loop-vectorize # Clang
Add either to Compiler Explorer’s flags and you get a line per loop explaining the refusal in English. This is the single most useful diagnostic in this area and it is almost unknown outside compiler circles. Everything below is context for interpreting what it says.
The four blockers
Varies: the single obstacle
| Blocker | Example | Fix | Cost of the fix |
|---|---|---|---|
| Aliasing | out[i] = in[i]*2 with two pointers | restrict, or Rust &mut | None — it is information, not a change |
| FP reduction | float s; s += a[i]; | -ffast-math, or restructure | Different results |
| Loop-carried dependency | a[i] = a[i-1]+1 | Change the algorithm | Real work; sometimes impossible |
| Unknown trip count | while (*p) p++; | Give it a length | Usually an API change |
Only the first has a free fix. The second changes your answers. The third and fourth need real changes. This is why 'just vectorise it' is not advice.
Aliasing is the common one and the only free win. Two pointer parameters might overlap, so processing four elements at once could differ from processing one at a time. The compiler emits a runtime check plus a scalar fallback (3.4), or gives up.
Floating-point reductions are refused for a correctness reason, not timidity. Vectorising a sum means four partial sums combined at the end — a different grouping — and FP addition is not associative, so the result can differ. The compiler will not silently change your numbers.
int sum_i(const int *a, int n) { int s=0; for(int i=0;i<n;i++) s+=a[i]; return s; }
float sum_f(const float *a, int n) { float s=0; for(int i=0;i<n;i++) s+=a[i]; return s; }Identical structure. One element type differs.
The integer one vectorises. The float one does not.
Integer addition is associative — wrapping two’s complement arithmetic gives the same answer in any grouping — so reordering is legal and the compiler takes it.
Float addition is not. (a+b)+c can differ from a+(b+c) because each operation
rounds. So the compiler refuses.
This is the cleanest possible demonstration that the obstacle is semantics, not compiler quality. Two loops that look identical; one legal transformation and one illegal one.
Three ways forward, with honest costs:
-ffast-math— permits the reordering globally. Results change slightly, and it enables other assumptions too (no NaNs, no infinities) that can break code which checks for them. Not a free flag.-fassociative-math— narrower, permits just this.- Restructure by hand into four accumulators, which makes the reordering explicit in your source. You have then chosen the grouping, which is more honest than letting a flag choose it, and it documents that you accepted the numerical consequence.
For most code the difference is far below what you care about. For financial or scientific code it may not be, and the compiler cannot know which you are — which is exactly why it asks.
Where Rust has a structural advantage
The aliasing blocker is where the borrow checker pays off in generated code.
In C, telling the compiler two pointers do not overlap means writing restrict,
and nothing checks that you are right. Lying is undefined behaviour with no
diagnostic.
In Rust, &mut T is exclusivity, enforced at compile time. rustc emits
LLVM’s noalias on those references, so the compiler gets the information for free
and it cannot be wrong.
So the practical situation is: Rust often vectorises where equivalent C needs a manual annotation. This is a genuine, measurable benefit of the ownership model that has nothing to do with memory safety, and it is a good thing to be able to point at when someone asks what the borrow checker is for.
You have a hot loop over f64 that will not vectorise. Your team wants
-ffast-math added globally to the build.
Write your response. Cover: what it would actually permit beyond reassociation, what you would want to know about the code first, and a narrower alternative.
What you should now be able to say
- The compiler wants to vectorise and is blocked for nameable reasons.
- Ask it with
-fopt-info-vec-missedor-Rpass-missed=loop-vectorize. - Four blockers: aliasing, FP reduction, loop-carried dependency, unknown trip count.
- Only aliasing has a free fix;
-ffast-mathchanges your results and permits more than reassociation. - Integer reductions vectorise and float ones do not — semantics, not compiler quality.
- Rust’s
&mutgivesnoaliasfor free, so it vectorises where C needsrestrict.