Every SIMD skill so far baked a width into the code: 32 bytes per AVX2 step, 16 per NEON step, then a scalar tail for the rest. SVE (the Scalable Vector Extension) removes both. The hardware picks a vector length anywhere from 128 to 2048 bits (a multiple of 128), your code asks for it at run time, and predicates switch off the lanes past the end, so the last partial vector runs through the same loop body as all the others.
| machine | SVE vector length |
|---|---|
| Neoverse N2 / V2 (e.g. Graviton4, Grace) | 128 bits |
| Neoverse V1 (Graviton3) | 256 bits |
| Fujitsu A64FX (Fugaku) | 512 bits |
this course's qemu (-cpu max) |
512 bits by default, 128–2048 on request |
SVE is optional, so — like AVX2 — enable it per function:
__attribute__((target("+sve"))), and include <arm_sve.h>.
1. Registers: z, p and the length
Code never knows VL at compile time. It asks:
svcntb() bytes per vector (VL / 8)
svcnth() 16-bit lanes svcntw() 32-bit lanes svcntd() 64-bit lanes#include <arm_sve.h>
#include <cstdio>
#include <sys/prctl.h>
__attribute__((target("+sve"))) void show() {
std::printf("VL = %d bits: %d bytes, %d int32 lanes, %d int64 lanes\n",
(int)svcntb() * 8, (int)svcntb(), (int)svcntw(), (int)svcntd());
}
int main() {
show();
prctl(PR_SVE_SET_VL, 16); // Linux lets a process lower its vector length
show();
}The SVE types (svint32_t, svfloat32_t, svbool_t…) are sizeless: no
sizeof, no arrays of them, no struct members. They live in locals,
parameters and return values only — the compiler can't lay out an object whose
size it doesn't know.
2. The loop: predicates instead of tails
for (std::size_t i = 0; i < n; i += svcntw()) { // step: one vector of int32
svbool_t pg = svwhilelt_b32(i, n); // lane k active while i + k < n
svint32_t v = svld1(pg, p + i); // inactive lanes: not read (and set to 0)
…
svst1(pg, out + i, result); // inactive lanes: not written
}svwhilelt_b32(i, n) is "while less than": lane k is on iff i + k < n.
Predicated loads and stores don't touch inactive lanes, so the loop never
reads or writes past the end — the same code is correct for every n and every
VL. svptrue_b32() makes all lanes active; use it only when you know a whole
vector is valid.
#include <arm_sve.h>
#include <cstdint>
#include <cstdio>
__attribute__((target("+sve")))
void axpy(std::int32_t a, const std::int32_t* x, std::int32_t* y, std::size_t n) {
for (std::size_t i = 0; i < n; i += svcntw()) {
svbool_t pg = svwhilelt_b32(i, n);
svint32_t vy = svld1(pg, y + i);
svst1(pg, y + i, svmla_x(pg, vy, svld1(pg, x + i), a)); // y + x·a
}
}
int main() {
std::int32_t x[37], y[38];
for (int i = 0; i < 37; ++i) { x[i] = i; y[i] = 1; }
y[37] = -999; // guard: must survive
axpy(2, x, y, 37);
std::printf("y[0]=%d y[36]=%d guard=%d\n", y[0], y[36], y[37]);
}3. Merging, zeroing, don't-care
Most arithmetic intrinsics come in three predicated forms:
svadd_m(pg, a, b) inactive lanes keep a (merging) add z0.s, p0/m, z0.s, z1.s
svadd_z(pg, a, b) inactive lanes become 0 (zeroing) movprfx z0.s, p0/z, z0.s ; add …
svadd_x(pg, a, b) inactive lanes: unspecified (don't care) add z0.s, z0.s, z1.s_m keeps a there, _z writes 0, _x leaves whatever is cheapest — fine only if you never look at those lanes again.Choose by what happens to the inactive lanes afterwards:
- results you'll store under the same predicate →
_x(cheapest: the compiler may drop the predicate entirely); - accumulators carried across iterations →
_m. With_z, the last partial iteration would wipe the inactive lanes of the accumulator and lose everything they had summed; _zwhen you need clean zeros, e.g. before an unpredicated reduction.
4. Compares produce predicates
svcmpgt(pg, v, t) returns an svbool_t, not a vector of all-ones lanes —
the job x86 and NEON give to masks in vector registers (and to movemask)
belongs to predicate registers here:
svcmpgt / svcmpeq / … predicate of lanes where the compare holds
svcntp_b32(pg, p) count of active lanes of p (cntp) — popcount of a mask
svptest_any(pg, p) any lane true? (ptest + b.any)
svbrkb / svlastb lanes before the first true one / last active element
svsel(p, a, b) per-lane select (sel)svcmpgt(pg, v, 5) sets a predicate bit per lane; svcntp counts them (3), svptest_any asks whether any is set, svbrkb keeps the lanes before the first true one.5. Widening and reductions
- Widening loads:
svld1sw_s64(pg, p)loads int32s and sign-extends each into an int64 lane (ld1sw z.d);svld1ub_u32loads bytes into 32-bit lanes. So a 64-bit accumulation steps bysvcntd()withsvwhilelt_b64. - Reductions take a predicate and ignore inactive lanes:
svaddv,svmaxv,svminv,svandv…svaddvon 8/16/32-bit integer lanes returns a 64-bit sum (saddv d0, p0, z0.s) — but only the final reduction widens; lanes you accumulated in 32 bits may already have wrapped. - Floating-point
svaddvis a tree reduction (faddv), so its rounding differs from a sequential loop;svadda(fadda) adds strictly in order.
6. Loops that can't know n: first-faulting loads
A vectorized strlen doesn't know where the string ends, so reading a whole
vector might cross into an unmapped page. SVE's answer is the first-faulting
load svldff1: lanes up to the first one that would fault are loaded, and
instead of trapping, the faulting lane and everything after it are marked
inactive in the FFR (svrdffr). The loop processes the lanes it got and
continues from there. NEON code has to align its loads to stay within a page
instead.
'\0' and stops; otherwise it would continue from the first lane it didn't get. NEON avoids the fault only by aligning its loads.7. The compiler and SVE
Predication makes auto-vectorization cheaper: there's no epilogue to justify,
so GCC's -O2 cost model vectorizes loops for SVE that it leaves scalar for
NEON (an unknown-n a[i] += b[i] is the classic case). The generated loop
looks exactly like the hand-written one:
whilelo p7.s, wzr, w2 // first predicate
.L8: ld1w z31.s, p7/z, [x0, x3, lsl 2]
ld1w z30.s, p7/z, [x1, x3, lsl 2]
add z30.s, z30.s, z31.s
st1w z30.s, p7, [x0, x3, lsl 2]
add x3, x3, x4 // x4 = cntw
whilelo p7.s, w3, w2 // next predicate, sets flags
b.any .L8 // loop while any lane is activeUse the explorer's AArch64 target with -march=armv8.2-a+sve to watch
loops vectorize; intrinsics remain the tool for what the vectorizer won't do
(early exits with first-faulting loads, predicate tricks, gathers, specific
widening).
SVE2 (Armv9) extends SVE to most of NEON's integer and DSP operations
(widening/narrowing pairs, complex arithmetic, match/histcnt for
string and histogram work), making SVE a full replacement rather than an HPC
add-on. SME adds matrix tiles on top — a topic of its own.
8. Testing vector-length agnostic code
Code that passes at one VL can still assume it. Linux lets a process change
its own vector length with prctl(PR_SVE_SET_VL, bytes), and qemu emulates
every length up to 2048 bits, so this skill's tests rerun everything at 128,
256, 384, 512, 1024 and 2048 bits. A hard-coded i += 16 passes at 512 and
fails everywhere else.