AArch64 NEON and SIMD Basics
Advanced SIMD, commonly called NEON, lets one instruction operate on several small values held in a 128-bit vector register. It is part of the AArch64 architectural family, but vectorization is not automatic proof of speed or correctness. Choose it after defining element type, overflow behavior, aliasing, alignment, target feature set, and a scalar reference result.
V registers, lanes, and arrangements
AArch64 has vector registers v0 through v31, each 128 bits wide. The same physical register can be viewed as a scalar floating-point value or as lanes of different widths. Arrangement syntax makes the interpretation visible: v0.16b is sixteen 8-bit lanes, v0.8h eight 16-bit lanes, v0.4s four 32-bit lanes, and v0.2d two 64-bit lanes. A lower 64-bit view may be written v0.8b, v0.4h, v0.2s, or v0.1d.
| Arrangement | Lanes | Typical data |
|---|---|---|
.16b | 16 × 8-bit | Bytes, pixels, masks |
.8h | 8 × 16-bit | Audio samples, short integers |
.4s | 4 × 32-bit | Integers or single-precision floats |
.2d | 2 × 64-bit | Long integers or double-precision floats |
The letters describe lane width, not signedness. Signed and unsigned semantics come from instruction choice, particularly widening, comparison, and saturation operations. Floating point has its own instructions and IEEE-754 concerns such as rounding and NaNs. Treat the arrangement as part of a value’s type: reinterpreting bits can be intentional, but it is not a numeric conversion.
A controlled vector addition
// void add4(const int *a, const int *b, int *out)
// x0=a, x1=b, x2=out. Caller guarantees four valid non-overlapping ints.
add4:
ldr q0, [x0] // four 32-bit lanes
ldr q1, [x1]
add v0.4s, v0.4s, v1.4s // modulo-2^32 integer lane addition
str q0, [x2]
ret
q0 names the full 128-bit register for load/store syntax; v0.4s declares the arithmetic lane arrangement. The routine processes exactly four elements, not an arbitrary array. In C, signed overflow is undefined while the assembly addition wraps, so a robust interface should use unsigned integers or document and test its required overflow behavior. It also assumes the output does not overlap input in a way that changes the intended result. These details matter more than the four-instructions appearance.
Loads, lanes, and the tail problem
Vector loads and stores move contiguous bytes; they do not validate pointers or bounds. A 16-byte ldr q0, [x0] requires that a complete 16-byte accessible region exists. Hardware and platform alignment behavior can be more permissive than an algorithm’s contract, but aligned allocation may still help performance and portability. Do not vector-load “a little past” the logical end merely because a test happened to allocate extra space.
Most loops need a vector body plus a scalar or safely masked tail. Fixed-width NEON lacks the general predicate model of scalable SVE, so careless tail handling is a common source of out-of-bounds reads. Keep a count in elements, increment pointers by the vector byte width, and prove the loop condition leaves enough elements for every full load. Then process remaining elements with a simple scalar loop. Benchmark only after tests cover zero length, lengths smaller than one vector, unaligned valid buffers, and boundary-sized tails.
Conversions and data layout
Widening is often needed before arithmetic. For example, adding two unsigned bytes into an unsigned byte wraps unless the operation widens or uses an explicitly saturating instruction. Image code may require saturation; a checksum may deliberately require modular arithmetic. State that choice in the function name and tests. Narrowing can discard high bits, round, or saturate depending on the instruction, so it should be visible at the point values change representation.
Interleaved structures and planar arrays require different loads and lane rearrangements. Four adjacent red values are easy to process as .4s; four RGBA pixels are an interleaved byte stream and may require deinterleaving before per-channel arithmetic. Endianness, external file layout, and lane order are separate questions. Draw a four-element input/output example and test it byte-for-byte before optimizing a rearrangement sequence.
NEON is fixed at 128 bits. Do not confuse it with SVE/SVE2, whose vector length is implementation-dependent and whose programming model includes predicates. A routine written for NEON has predictable lane count, but it still requires a target that supports the instructions and an ABI-compatible build. Feature detection and dispatch belong in platform-aware library code rather than an unconditional instruction in a general-purpose binary.
Build confidence in layers
First test a scalar specification with inputs whose expected answers are written down. Next test the vector routine against that reference across randomized and boundary cases in an authorized test program. Include distinct buffers and any overlap patterns the API promises to support. Only then measure performance. This sequence catches lane-order, tail, and overflow errors that can otherwise be hidden by impressive benchmark numbers.
Inspecting a compiler’s output is educational, but source-level intrinsics do not guarantee one particular instruction sequence. Different optimization levels, microarchitecture tuning, and surrounding code change scheduling and register allocation. Conversely, a visible NEON instruction does not prove a whole loop is vectorized: scalar setup, reductions, and tails remain. Keep a correctness test independent of the implementation so a future compiler upgrade is a routine revalidation, not a leap of faith.
Memory safety remains the first performance feature. Bounds checks, sanitizers in a higher-level harness, and clear ownership rules can find defects before vector code obscures them. Do not disable security checks or use unsafe benchmark inputs to obtain a cleaner timing. A correct scalar fallback is often the best behavior on small inputs or targets where the desired extension is unavailable.
Document the chosen vector width in comments and APIs. Future maintainers should know whether “four” means four bytes, four 32-bit lanes, or four logical records. Such precision also makes later ports to intrinsics, SVE, or another architecture a controlled engineering change rather than a transcription exercise.
When in doubt, favor a direct scalar expression with a test over an opaque shuffle sequence. The best SIMD implementation is one whose data movement, numerical behavior, and bounds are still reviewable by the next engineer.
Operations and ABI interaction
| Family | Examples | Correctness question |
|---|---|---|
| Lane arithmetic | add, sub, mul | Wrapping, widening, or saturation? |
| Widen/narrow | uxtl, sqxtn | What happens to out-of-range values? |
| Compare/select | cmeq, cmgt, bsl | Which signedness and mask convention? |
| Rearrange | zip, uzp, trn | Are lanes in the required order? |
| Reduction | addv, faddp | Does changed summation order affect results? |
AAPCS64 passes many FP/vector arguments in v0–v7. Vector registers have preservation rules too: broadly, v0–v7 and v16–v31 are temporary, while the low 64 bits of v8–v15 have callee-save requirements in the base procedure call standard. Read the current AAPCS64 wording before hand-writing an interface, especially if a function calls other code. A compiler intrinsic often expresses the intent while allowing correct register allocation and target selection.
Performance is measured, not assumed
SIMD can reduce instruction count, yet be slower when memory bandwidth, cache misses, conversion overhead, branching, or tiny input sizes dominate. Data layout matters: contiguous arrays are friendlier than scattered structures. Auto-vectorizers may already produce good NEON for simple loops when aliasing and bounds are clear. Start with readable scalar code and tests, inspect compiler vectorization reports or generated assembly, then use intrinsics before standalone assembly when possible. Compare equivalent builds on the actual permitted target, with warm-up, representative sizes, and correctness checks—not a single timing.
Floating-point vector results may differ from scalar results because reassociation, fused operations, rounding, and reduction order can change. Do not enable aggressive floating-point transformations unless their numerical contract permits them. Similarly, cryptographic, image, and signal workloads often need carefully specified byte order and saturation semantics; a vector register’s little-endian memory layout is not a license to skip format documentation.
Safe exercises
- Write down the lane count and byte width of
v3.8handv3.2d. - Make a scalar unsigned-four-word addition reference and compare it with
add4for all-zero, all-maximum, and mixed test vectors you create. - Design, on paper, a loop for 19 unsigned bytes: identify the full-vector iterations and scalar tail without reading beyond the array.
- Use compiler intrinsics for a known local array and inspect the object-file disassembly; explain any scalar fallback rather than forcing instructions.
The existing ARM64 GDB tutorial is a supplemental board-specific way to inspect registers in a program you built. It is not a guide to attaching to or running unknown programs.
dispelled