Skip to content

Commit ca842a4

Browse files
authored
more for piece-wise
1 parent 993e4c8 commit ca842a4

1 file changed

Lines changed: 98 additions & 1 deletion

File tree

‎_posts/2026-03-24-VectorAPI-control-flow.md‎

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,9 +127,106 @@ impact if the mask entries are `true` or `false`.
127127
On AVX512 we have some individual down-spikes.
128128
I suspect this might be due to [(mis)alignment causing multimodal performance](https://eme64.github.io/blog/2026/01/12/Alignment-Performance.html).
129129

130+
What we learn from this example:
131+
Generally, vectorization leads to speedups because of parallelization - but with a theoretical maximum of the vector length.
132+
On NEON where we have 16 byte elements in a vector this would mean we could only gain at most a factor of 16.
133+
But using masked vector operations, we can sometimes gain more performance:
134+
especially if we can avoid the branch misprediction penalty that the scalar implementation suffers from
135+
for some branch probabilities.
136+
130137
**Algorithm 2: pieceWise**
131138

132-
TODO
139+
This example is a bit more contrived, but it shows a very interesting effect.
140+
We want to apply a function `f(x)` to every input in the array `a`,
141+
and store the results in the array `r`.
142+
But the function `f` is piece-wise: for small input values we compute some multiplications, for high input values we compute some square roots.
143+
Here a plot of the function:
144+
145+
<img width="600" alt="piece-wise function f" src="https://github.com/user-attachments/assets/fb587786-9dba-4333-b8b4-119a1994db5c" />
146+
147+
It is important to say:
148+
computing square roots is very expensive compared to multiplications.
149+
150+
Reference implementation:
151+
```java
152+
for (int i = 0; i < a.length; i++) {
153+
float ai = a[i];
154+
if (ai < 1f) {
155+
float a2 = ai * ai;
156+
float a4 = a2 * a2;
157+
float a8 = a4 * a4;
158+
r[i] = a8;
159+
} else {
160+
float s2 = (float)Math.sqrt(ai);
161+
float s4 = (float)Math.sqrt(s2);
162+
float s8 = (float)Math.sqrt(s4);
163+
r[i] = s8;
164+
}
165+
}
166+
```
167+
168+
Vector API implementation (v1), unconditionally compute both branches for all lanes:
169+
```java
170+
for (i = 0; i < SPECIES_F.loopBound(a.length); i += SPECIES_F.length()) {
171+
var ai = FloatVector.fromArray(SPECIES_F, a, i);
172+
var mask = ai.compare(VectorOperators.LT, 1f);
173+
var a2 = ai.lanewise(VectorOperators.MUL, ai);
174+
var a4 = a2.lanewise(VectorOperators.MUL, a2);
175+
var a8 = a4.lanewise(VectorOperators.MUL, a4);
176+
var s2 = ai.lanewise(VectorOperators.SQRT);
177+
var s4 = s2.lanewise(VectorOperators.SQRT);
178+
var s8 = s4.lanewise(VectorOperators.SQRT);
179+
var v = s8.blend(a8, mask);
180+
v.intoArray(r, i);
181+
}
182+
// omitting scalar cleanup
183+
```
184+
185+
Vector API implementation (v2), unconditionally compute the multiplications for all lanes, but only compute the square roots if at least one lane needs it:
186+
```java
187+
for (i = 0; i < SPECIES_F.loopBound(a.length); i += SPECIES_F.length()) {
188+
var ai = FloatVector.fromArray(SPECIES_F, a, i);
189+
var mask = ai.compare(VectorOperators.LT, 1f);
190+
var a2 = ai.lanewise(VectorOperators.MUL, ai);
191+
var a4 = a2.lanewise(VectorOperators.MUL, a2);
192+
var a8 = a4.lanewise(VectorOperators.MUL, a4);
193+
var v = a8;
194+
// SQRT is expensive, so only call if it necessary
195+
if (!mask.allTrue()) {
196+
var s2 = ai.lanewise(VectorOperators.SQRT);
197+
var s4 = s2.lanewise(VectorOperators.SQRT);
198+
var s8 = s4.lanewise(VectorOperators.SQRT);
199+
v = s8.blend(a8, mask);
200+
}
201+
v.intoArray(r, i);
202+
}
203+
// omitting scalar cleanup
204+
```
205+
206+
Running on an `x64 AVX512` and an `aarch64 NEON` machine:
207+
208+
<img width="700" alt="piece-wise performance" src="https://github.com/user-attachments/assets/df288e08-e266-4ccc-9751-09aa7d38a34d" />
209+
210+
The scalar implementation has branches, so it is sensitive to the branch probability:
211+
212+
- Low (mostly `sqrt`): slow, becaue `sqrt` is slow.
213+
- High (mostly `mul`): fast, because `mul` is fast.
214+
- Middle (mixed): there is the classic branch misprediction penalty bump for NEON, though for AVX512 is is very faint.
215+
216+
The non-branching Vector API implementation (`v1`) is not sensitive to the branching probability.
217+
It is faster than the scalar implementation for some but not all branching probabilities.
218+
219+
The branching Vector API implementation (`v2`) is the fastest implementation in almost all cases,
220+
because it combines the benefits of vectorization with the benefits of branching to
221+
avoid square root computation when possible.
222+
But on NEON the scalar implementation seems to be slightly faster for branch probabilities around `0.125`.
223+
224+
What we can learn from this example:
225+
We should not apply vectorization blindly.
226+
Using masked vector operations sometimes means we have to execute both branches.
227+
If one branch is much more expensive than the other, this can mean that
228+
scalar branch prediction outperforms vectorized implementations,
229+
at least for some branch probabilities.
133230

134231
**Algorithm 3: find**
135232

0 commit comments

Comments
 (0)