FForge
← Labsaarch64-02AArch64 and SVE●●○○

NEON vs SVE codegen, and vector-length-agnostic execution under emulation

aarch64/02_neon_sve.cneeds: tools/a64.sh (clang cross + sysroot + qemu-user)3 steps
0/3 steps
what this lab teaches

Three loops (vector add, integer sum, float max) compiled twice: for armv8-a (NEON: fixed 128-bit vectors) and for armv8-a+sve (SVE: the vector length is decided by the hardware). The SVE binary is ONE binary that runs correctly at every vector length from 128 to 2048 bits, which qemu lets you prove by changing the length from the command line. n = 1003 elements on purpose: not a multiple of any vector length, so the remainder handling is exercised every time.

evidence to leave with
n = 1003 correct at 128/256/512/1024/2048 bits; tail-folded loop has no remainder
first, get the code
run this
$ git clone https://github.com/adit4443ya/debug_labs.git && cd debug_labs
then build it
run this
$ cd aarch64 && ./build.sh
produces bin/02_neon, bin/02_sve, bin/02_sve_tail, bin/02_neon.s, bin/02_sve.s
walk it, one step at a time
run this
$ source ../tools/a64.sh; a64s -O2 -march=armv8-a+sve -Rpass=loop-vectorize 02_neon_sve.c >/dev/null
what it printed on the machine that wrote this lab
vadd  : vectorized loop (vectorization width: vscale x 4, interleaved count: 2)
isum  : vectorized loop (vectorization width: vscale x 2, interleaved count: 2)
fmax_ : vectorized loop (vectorization width: vscale x 4, interleaved count: 1)
main  : vectorized loop (vectorization width: 4, ...)   (the fill loop, fixed width)
`vscale x 4` is LLVM's notation for "4 lanes per 128 bits, times the
runtime multiple vscale = VL/128". The IR type is <vscale x 4 x float>.

NEON (bin/02_neon.s, vadd): fixed 128-bit q registers, a scalar tail loop for the remainder: ...ldp q0, q1 ... fadd v0.4s ... stp ... (vector body, 8 floats per step) .LBB0_4: fadd s0, s0, s1 ; subs x8, x8, #1 ; b.ne .LBB0_4 (scalar remainder: up to 7 elements) SVE (bin/02_sve.s, vadd, default epilogue): the main loop uses z registers with an unpredicated ptrue body and an epilogue. SVE with tail folding (bin/02_sve_tail, -mllvm -prefer-predicate-over-epilogue=predicate-dont-vectorize): whilelo p0.s, xzr, x3 // p0[lane] = (lane < n) .LBB0_4: ld1w { z0.s }, p0/z, [x1, x10, lsl #2] // load active lanes, zero the rest ld1w { z1.s }, p0/z, [x2, x10, lsl #2] fadd z0.s, z0.s, z1.s st1w { z0.s }, p0, [x0, x10, lsl #2] // store active lanes only whilelo p0.s, x10, x9 // recompute the predicate for the next block No remainder loop at all: the final iteration simply has a partially true predicate. And no vector length appears anywhere in the code.

run this
$ a64run ./bin/02_neon
what it printed on the machine that wrote this lab
NEON build: VL= 128 bits ( 4 f32 lanes) | vadd mismatches=0 | isum=-5 (ok) | fmax=1002 (ok)
run this
$ for vl in 16 32 64 128 256; do a64sve $vl ./bin/02_sve; done      (VL in bytes)
what it printed on the machine that wrote this lab
SVE build : VL= 128 bits ( 4 f32 lanes) | vadd mismatches=0 | isum=-5 (ok) | fmax=1002 (ok)
SVE build : VL= 256 bits ( 8 f32 lanes) | vadd mismatches=0 | isum=-5 (ok) | fmax=1002 (ok)
SVE build : VL= 512 bits (16 f32 lanes) | vadd mismatches=0 | isum=-5 (ok) | fmax=1002 (ok)
SVE build : VL=1024 bits (32 f32 lanes) | vadd mismatches=0 | isum=-5 (ok) | fmax=1002 (ok)
SVE build : VL=2048 bits (64 f32 lanes) | vadd mismatches=0 | isum=-5 (ok) | fmax=1002 (ok)
run this
$ a64sve 64 ./bin/02_sve_tail
what it printed on the machine that wrote this lab
SVE build : VL= 512 bits (16 f32 lanes) | vadd mismatches=0 | isum=-5 (ok) | fmax=1002 (ok)

The same bytes, correct results at 4, 8, 16, 32 and 64 lanes, with 1003 elements. That is what "vector-length agnostic" means, and this is how you TEST it: emulation with several lengths is part of the validation of any SVE kernel, because real hardware gives you one length (Graviton3/Neoverse V1: 256; most others: 128) and bugs in remainder handling hide at the length you happen to own.

what qemu is and is not for

Correctness across vector lengths, ISA feature coverage (SVE2, SME: lab 05), reading real register state under gdb. NOT performance: a qemu-emulated loop tells you nothing about cycles; for that use llvm-mca with -mcpu=neoverse-v2 (lab 04 in codegen for the method) or real hardware.

the rules

E1. Ask -Rpass=loop-vectorize for both targets; `vscale x N` means SVE code was generated; a fixed number means NEON (or the fill loop). E2. Prefer tail folding for SVE loops with short or unknown trip counts (no epilogue, no scalar remainder), the default epilogue for long loops where an unpredicated body is faster; measure on hardware. E3. Test SVE code at several vector lengths under qemu before it meets a machine; include a length that does NOT divide your sizes.

take it further

1. Run bin/02_sve on `-cpu cortex-a57` (no SVE): SIGILL. Runtime dispatch (lab 04) is how a binary survives both. 2. Add a loop with an `if` inside (masked update) and compare NEON (bsl select) vs SVE (predicated ops). 3. Compile with -msve-vector-bits=256: fixed-length SVE; the code may hardcode 8 lanes; run it at VL 128 under qemu and see what breaks. ============================================================

read the source · aarch64/02_neon_sve.c
#include <stdio.h> #include <stdlib.h> #ifdef __ARM_FEATURE_SVE #include <arm_sve.h> #endif __attribute__((noinline)) void vadd(float *c, const float *a, const float *b, long n) { for (long i = 0; i < n; i++) c[i] = a[i] + b[i]; } __attribute__((noinline)) long isum(const int *a, long n) { long s = 0; for (long i = 0; i < n; i++) s += a[i]; return s; } __attribute__((noinline)) float fmax_(const float *a, long n) { float m = a[0]; for (long i = 1; i < n; i++) m = a[i] > m ? a[i] : m; return m; } int main(int argc, char **argv) { long n = argc > 1 ? atol(argv[1]) : 1003; /* deliberately NOT a multiple of any vector length */ float *a = malloc(n * 4), *b = malloc(n * 4), *c = malloc(n * 4); int *v = malloc(n * 4); for (long i = 0; i < n; i++) { a[i] = (float)i; b[i] = (float)(2 * i); v[i] = (int)(i % 7) - 3; } vadd(c, a, b, n); long bad = 0; for (long i = 0; i < n; i++) bad += (c[i] != 3.0f * (float)i); long s = isum(v, n), expect = 0; for (long i = 0; i < n; i++) expect += (i % 7) - 3; float m = fmax_(a, n); #ifdef __ARM_FEATURE_SVE printf("SVE build : VL=%4llu bits (%2llu f32 lanes) | vadd mismatches=%ld | isum=%ld (%s) | fmax=%.0f (%s)\n", (unsigned long long)svcntb() * 8, (unsigned long long)svcntw(), bad, s, s == expect ? "ok" : "WRONG", (double)m, m == (float)(n - 1) ? "ok" : "WRONG"); #else printf("NEON build: VL= 128 bits ( 4 f32 lanes) | vadd mismatches=%ld | isum=%ld (%s) | fmax=%.0f (%s)\n", bad, s, s == expect ? "ok" : "WRONG", (double)m, m == (float)(n - 1) ? "ok" : "WRONG"); #endif return bad != 0 || s != expect; }
◇
Finish the lab: write the evidence note

A lab is done when you can say what it showed on your machine — not when you have read it.