Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Assembly (x86-64, AArch64, RISC-V)  Module 10 – Floating point and SIMD

Your first SIMD loop

Write a NEON int32 vector add with a scalar tail, read the SSE2, AVX2 and RVV versions back from the assembler, and prove the tail is right.

  • Intermediate
  • 20 minutes
  • Examples run with Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 6 of 20 minutes free
Free: A vector loop adds several lanes at once · The scalar tail handles the leftover · The same loop on x86-64 and RISC-V · Practise. With a pass: 2 in-depth parts and 2 interview answers, about 14 minutes.

What you will learn

  • Write a NEON vector loop that adds two int32 arrays with a scalar tail
  • Handle elements left over when the length is not a multiple of the vector width
  • Compare the same loop in SSE2, AVX2 and the RISC-V V extension
  • Explain why movdqa faults on a misaligned load while movdqu does not
  • Check a SIMD routine against a scalar reference for lengths that cover every tail count

Before you start

What you’ll learn · Before you start

What you will learn

  • Write a NEON vector loop that adds two int32 arrays with a scalar tail
  • Handle elements left over when the length is not a multiple of the vector width
  • Compare the same loop in SSE2, AVX2 and the RISC-V V extension
  • Explain why movdqa faults on a misaligned load while movdqu does not
  • Check a SIMD routine against a scalar reference for lengths that cover every tail count

Before you start

On this page

A scalar loop that adds two arrays of twenty 32-bit integers runs twenty add instructions, one per element. On a 128-bit SIMD: Single Instruction, Multiple Data: one instruction that operates on several values at once inside a wide register. The processor divides the register into equal-sized lanes and runs the operation on every lane together. More in “SIMD concepts and CPU feature detection” unit one add covers four 32-bit lanes, so twenty elements take five vector passes; twenty-one take five passes and one scalar step for the leftover, and twenty-two take five and two.

Below, that loop is written in AArch64 Advanced SIMD, and its SSE2, AVX2 and RISC-V V versions are assembled and read back. A vector body does the bulk, a scalar loop tail: The leftover iterations of a vector loop that handle fewer elements than one vector pass covers, usually run as a short scalar loop. finishes the rest, and a plain scalar loop checks every result.

A vector loop adds several lanes at once

The NEON vector loop: A loop body that uses one SIMD instruction to process several lanes of a vector register per pass, in place of several scalar instructions. below adds two int32 arrays into a third. The vector register: A register wider than a general-purpose one that holds several values side by side. A SIMD instruction treats it as a row of equal-sized slots and operates on every slot in parallel. More in “SIMD concepts and CPU feature detection” view v0.4s has four 32-bit A lane is one slot of a vector register; a 4s view treats a 128-bit register as four 32-bit slots., and add v2.4s, v0.4s, v1.4s performs four lane-wise adds in one instruction. The post-index [x1], #16 on ld1 reads 16 bytes and moves x1 forward, so the loop is the familiar count-down with no separate pointer adds:

vadd.s, the NEON vector body with its scalar tail, and its driver vadd.c

neon/vadd.s

// A 128-bit NEON int32 add with a scalar tail (AArch64, Apple silicon).
//
// void vadd_neon(int32_t *c, const int32_t *a, const int32_t *b, size_t n)
//   - n / 4 vector passes of 4 int32 lanes each,
//   - then n % 4 scalar passes for the leftover elements.
//
// Mach-O: the exported name needs a leading underscore. AAPCS64 puts pointers in
// x0, x1, x2 and the count in x3; the function clobbers only caller-saved registers,
// so no stack frame is needed.

        .text
        .p2align 2
        .globl  _vadd_neon
_vadd_neon:
        lsr     x4, x3, #2              // x4 = n / 4, the number of full vectors
        cbz     x4, 2f                  // nothing to vectorise: go straight to the tail
1:      ld1     {v0.4s}, [x1], #16      // post-index: load 4 int32s from a, advance by 16
        ld1     {v1.4s}, [x2], #16      // load 4 int32s from b
        add     v2.4s, v0.4s, v1.4s     // lane-wise 32-bit add on 4 lanes at once
        st1     {v2.4s}, [x0], #16      // store the 4 sums and advance c by 16
        subs    x4, x4, #1              // one less vector pass to do
        b.ne    1b
2:      ands    x3, x3, #3              // x3 = n % 4, the leftover count
        b.eq    4f                      // no tail: done
3:      ldr     w5, [x1], #4            // scalar tail: one int32 at a time
        ldr     w6, [x2], #4
        add     w5, w5, w6
        str     w5, [x0], #4
        subs    x3, x3, #1
        b.ne    3b
4:      ret

vadd.c

// Drives vadd_neon for several values of n and checks each result against a plain
// C scalar loop.  Prints one line per n with the vector pass count, the tail count
// and whether the result matched.

#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void vadd_neon(int32_t *c, const int32_t *a, const int32_t *b, size_t n);

static void scalar_ref(int32_t *c, const int32_t *a, const int32_t *b, size_t n) {
    for (size_t i = 0; i < n; i++) c[i] = a[i] + b[i];
}

int main(void) {
    static const size_t cases[] = {0, 3, 4, 10, 17};
    enum { MAX = 32 };
    int32_t a[MAX], b[MAX], c[MAX], ref[MAX];

    printf("  n   passes   tail   match\n");
    int all_ok = 1;
    for (size_t k = 0; k < sizeof cases / sizeof cases[0]; k++) {
        size_t n = cases[k];
        for (size_t i = 0; i < n; i++) { a[i] = (int32_t)(i + 1); b[i] = (int32_t)(10 * (i + 1)); }
        // Fill the slots after the last element with -1 and check them after the
        // call, so a store past c[n - 1] makes the row print NO.
        for (size_t i = n; i < MAX; i++) c[i] = -1;
        memset(c, 0, n * sizeof c[0]);
        vadd_neon(c, a, b, n);
        scalar_ref(ref, a, b, n);
        int ok = (memcmp(c, ref, n * sizeof c[0]) == 0);
        for (size_t i = n; i < MAX; i++) if (c[i] != -1) ok = 0;
        printf("%3zu   %5zu   %4zu   %s\n", n, n / 4, n % 4, ok ? "yes" : "NO");
        all_ok &= ok;
    }
    printf("summary: %s on every n\n", all_ok ? "match" : "mismatch");
    return all_ok ? 0 : 1;
}

The vector body runs n / 4 times; when n is not a multiple of four, a short scalar loop adds the rest one element at a time.

vadd for five values of n, each checked against a C scalar loop Python · neon_vadd_session.py
import pathlib
import sys

sys.path.insert(0, str(pathlib.Path(__file__).parent / "sessions"))
import session

# Build the NEON vadd with its scalar tail, run it for n = 0, 3, 4, 10 and 17,
# and check each result against a plain C scalar loop.  A row says yes only when
# the n sums match and every slot after them still holds -1.
session.run("""
clang -O1 -o vadd vadd.c neon/vadd.s
./vadd
""")

Output

$ clang -O1 -o vadd vadd.c neon/vadd.s
$ ./vadd
  n   passes   tail   match
  0       0      0   yes
  3       0      3   yes
  4       1      0   yes
 10       2      2   yes
 17       4      1   yes
summary: match on every n

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 neon_vadd_session.py

Every row says yes, including n = 0 (zero vector passes, zero tail passes). The driver, vadd.c, also fills the slots past the end with -1 and checks them, so a stray write would print NO.

Checkpoint

Check: The vector body

  1. One question How many 32-bit lanes does add v2.4s, v0.4s, v1.4s add in one pass?

    Type a number.

    A hint

    The .4s view treats a 128-bit register as a group of 32-bit slots.

    Show the answer: One question

    Answer: 4 lanes

    A 128-bit NEON register has 4 lanes of 32 bits, 8 of 16, 16 of 8 or 2 of 64. The .4s form of add writes four lane-wise 32-bit sums per instruction, which is why the recorded row for n = 10 shows 2 vector passes.

The scalar tail handles the leftover

The vector body stops when its pass counter reaches zero, and the scalar tail then runs n % 4 times. The match column of the run compares each result with a scalar reference: A plain non-SIMD implementation of the same function, used to check a vector routine's output lane by lane against an obviously correct one., a plain C loop over the same inputs.

Interactive · stage by stage Two vector passes and a two-element scalar tail Step through vadd_neon on n = 10 int32 values. The 128-bit NEON add writes 4 lanes each pass; the leftover 2 go through a scalar loop.
Two vector passes and a two-element scalar tail, step 6 of 6. Showing Array of n = 10 int32, Vector pass 1, Vector pass 2, Scalar step 1, Scalar step 2, Match against C ref. In focus: Match against C ref.Array of n = 10 int322 full vectors + 2 leftoverVector pass 1lanes 0, 1, 2, 3Vector pass 2lanes 4, 5, 6, 7Scalar step 1lane 8Scalar step 2lane 9Match against C ref11, 22, ..., 99, 110

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Split n into vector and tail. n / 4 is 2, so the vector body runs twice; n % 4 is 2, so the scalar tail runs twice. The function tests both counts first: cbz skips the vector body when its count is 0, and b.eq skips the tail.
  2. First four lanes in parallel. ld1 reads 4 int32s from a with post-index +16, ld1 reads 4 from b, add v2.4s does the four 32-bit adds in one instruction, and st1 writes them to c. One pass processes lanes 0 to 3.
  3. Pointers now at lane 4. The post-index of ld1 and st1 already moved a, b and c forward by 16 bytes. Pass 2 reads lanes 4 to 7 of the arrays without any extra add on the pointers.
  4. Two lanes left over. After pass 2 subs brings the vector counter x4 to 0, so b.ne falls through. ands x3, x3, #3 leaves n % 4 = 2 and clears the Z flag, so b.eq does not skip: the scalar loop runs.
  5. One int32 at a time. Each scalar pass loads one int32 from a and one from b with ldr, adds them in w5 and stores the sum with str; the loads and the store post-index by 4. Lane 8 goes through, then lane 9.
  6. Match a scalar loop. vadd.c sets a[i] = i + 1 and b[i] = 10(i + 1), so the sums for n = 10 are 11, 22, ..., 99, 110: eight from the vector body, two from the tail. The recorded run says yes on every n.

The step-through follows the recorded n = 10 row: two vector passes, two scalar steps and the comparison with the C loop. A multiple of four needs no scalar step, as the n = 4 row shows, and a length below four needs no vector pass, as n = 3 shows. The function skips a loop whose count is zero, so one body handles every length: cbz x4 jumps over the vector body when n / 4 is 0, and ands with b.eq jumps over the tail when n % 4 is 0.

Checkpoint

Check: The scalar tail

  1. One question For n = 17, the recorded run printed 4 vector passes and how many scalar tail passes?

    Type a number.

    A hint

    The tail does n % 4 passes.

    Show the answer: One question

    Answer: 1 passes

    17 % 4 is 1, so the vector body runs 4 times and the scalar tail runs once. The n = 17 row of the recorded run reads 4 vector passes, 1 tail pass and match yes.

The same loop on x86-64 and RISC-V

SSE2 is the 128-bit baseline of x86-64. The loop uses movdqu for both loads, paddd for four 32-bit lane-wise adds and another movdqu for the store. An unaligned load: A load from any byte address, with no alignment requirement; its aligned counterpart faults when the address is not a multiple of the operand size. such as movdqu reads any address; its aligned sibling movdqa needs a 16-byte-aligned one and raises a general-protection exception: The x86 exception #GP, raised for a protection violation such as an aligned vector instruction like movaps whose memory operand is not on a 16-byte boundary. Linux delivers it to a user program as SIGSEGV. More in “Stack alignment and the crashes it causes” otherwise. Both x86 listings show the vector body only, for a length that fills whole vectors.

AVX2 brings the integer add to the 256-bit ymm registers, so each pass adds eight 32-bit lanes with vpaddd ymm0, ymm0, [rdx], a three-operand VEX form. The loop ends with vzeroupper, which the Intel manual recommends when AVX code meets legacy SSE code, to avoid a false dependency on the upper register halves.

The RISC-V V extension picks a different shape. vsetvli t0, a3, e32, m1, ta, ma asks for 32-bit elements and writes into t0 how many lanes this pass gets. When fewer than a full register of elements remain, t0 is simply smaller, so the same body handles the last pass and no scalar tail exists. The next lesson covers this style for RVV, SVE and SME.

The same vadd on x86-64 and RISC-V

SSE2 · x86/vadd_sse2.asm

; SSE2 int32 add on 4 lanes per pass (Linux System V AMD64, assembled with NASM).
; void vadd_sse2(int32_t *c, const int32_t *a, const int32_t *b, size_t n), n % 4 == 0.
; System V puts the four arguments in rdi, rsi, rdx and rcx.
;
; The unaligned move movdqu reads 16 bytes from any address.  Its aligned sibling
; movdqa needs a 16-byte-aligned address, or the CPU raises a #GP (SIGSEGV on Linux).
; b is loaded with movdqu too: a legacy SSE paddd with a memory operand would need
; that operand 16-byte aligned.

default rel

section .text
        global  vadd_sse2
vadd_sse2:
        shr     rcx, 2                  ; rcx = n / 4
        jz      .done
.loop:  movdqu  xmm0, [rsi]             ; 4 int32s from a (unaligned load is fine)
        movdqu  xmm1, [rdx]             ; 4 int32s from b
        paddd   xmm0, xmm1              ; lane-wise 32-bit add
        movdqu  [rdi], xmm0             ; store 16 bytes into c
        add     rsi, 16
        add     rdx, 16
        add     rdi, 16
        dec     rcx
        jnz     .loop
.done:  ret

section .note.GNU-stack noalloc noexec nowrite progbits

AVX2 · x86/vadd_avx2.asm

; AVX2 int32 add on 8 lanes per pass (Linux System V AMD64, assembled with NASM).
; void vadd_avx2(int32_t *c, const int32_t *a, const int32_t *b, size_t n), n % 8 == 0.
;
; AVX uses VEX-encoded instructions on 256-bit ymm registers.  vzeroupper on the way
; out zeroes bits 255..128 of ymm0 to ymm15: the Intel manual recommends it "when
; transitioning between AVX and legacy SSE code" to avoid false dependencies.

default rel

section .text
        global  vadd_avx2
vadd_avx2:
        shr     rcx, 3                  ; rcx = n / 8
        jz      .out
.loop:  vmovdqu ymm0, [rsi]             ; 8 int32s from a
        vpaddd  ymm0, ymm0, [rdx]       ; AVX2 add, three-operand VEX form; b read in place
        vmovdqu [rdi], ymm0             ; 32 bytes into c
        add     rsi, 32
        add     rdx, 32
        add     rdi, 32
        dec     rcx
        jnz     .loop
.out:   vzeroupper
        ret

section .note.GNU-stack noalloc noexec nowrite progbits

RVV · riscv/vadd_rvv.s

// RISC-V V 1.0 strip-mined int32 add (Linux riscv64, assembled with llvm-mc -mattr=+v).
//
// void vadd_rvv(int32_t *c, const int32_t *a, const int32_t *b, size_t n)
//   a0 = c, a1 = a, a2 = b, a3 = n.  LP64D.
//
// vsetvli sets vl, the number of elements this pass gets (never more than VLMAX for
// e32 m1, and exactly the count left once that count is VLMAX or less).  No scalar
// tail is needed: the last pass simply runs over fewer elements.

        .text
        .globl  vadd_rvv
vadd_rvv:
1:      vsetvli t0, a3, e32, m1, ta, ma // t0 = vl for the a3 elements still to do
        vle32.v v0, (a1)                // load t0 int32s from a
        vle32.v v1, (a2)                // load t0 int32s from b
        vadd.vv v2, v0, v1              // lane-wise add on t0 lanes
        vse32.v v2, (a0)                // store t0 int32s into c
        sub     a3, a3, t0              // n -= elements done this pass
        slli    t1, t0, 2               // t1 = t0 * sizeof(int32_t)
        add     a1, a1, t1
        add     a2, a2, t1
        add     a0, a0, t1
        bnez    a3, 1b
        ret

        .section .note.GNU-stack,"",@progbits
The three sources assembled and read back with llvm-objdump Python · cross_asm_session.py
import pathlib
import sys

sys.path.insert(0, str(pathlib.Path(__file__).parent / "sessions"))
import session

# Assemble the x86-64 and riscv64 versions of the same vadd as Linux objects and
# read them back with llvm-objdump.  The listings show what the assemblers emitted;
# the functions themselves run only on their own processors.
# Each sed command prints from the function label to the end of the listing.
session.run("""
nasm -f elf64 -o vadd_sse2.o x86/vadd_sse2.asm && echo "SSE2 assembled"
llvm-objdump -d --no-show-raw-insn -M intel vadd_sse2.o | sed -n '/<vadd_sse2>:/,$p'
nasm -f elf64 -o vadd_avx2.o x86/vadd_avx2.asm && echo "AVX2 assembled"
llvm-objdump -d --no-show-raw-insn -M intel vadd_avx2.o | sed -n '/<vadd_avx2>:/,$p'
llvm-mc --triple=riscv64-linux-gnu -mattr=+d,+v -filetype=obj -o vadd_rvv.o riscv/vadd_rvv.s && echo "RVV assembled"
llvm-objdump -d --mattr=+d,+v --no-show-raw-insn vadd_rvv.o | sed -n '/<vadd_rvv>:/,$p'
""")

Output

$ nasm -f elf64 -o vadd_sse2.o x86/vadd_sse2.asm && echo "SSE2 assembled"
SSE2 assembled
$ llvm-objdump -d --no-show-raw-insn -M intel vadd_sse2.o | sed -n '/<vadd_sse2>:/,$p'
0000000000000000 <vadd_sse2>:
       0:      	shr	rcx, 0x2
       4:      	je	0x27 <vadd_sse2.done>

0000000000000006 <vadd_sse2.loop>:
       6:      	movdqu	xmm0, xmmword ptr [rsi]
       a:      	movdqu	xmm1, xmmword ptr [rdx]
       e:      	paddd	xmm0, xmm1
      12:      	movdqu	xmmword ptr [rdi], xmm0
      16:      	add	rsi, 0x10
      1a:      	add	rdx, 0x10
      1e:      	add	rdi, 0x10
      22:      	dec	rcx
      25:      	jne	0x6 <vadd_sse2.loop>

0000000000000027 <vadd_sse2.done>:
      27:      	ret
$ nasm -f elf64 -o vadd_avx2.o x86/vadd_avx2.asm && echo "AVX2 assembled"
AVX2 assembled
$ llvm-objdump -d --no-show-raw-insn -M intel vadd_avx2.o | sed -n '/<vadd_avx2>:/,$p'
0000000000000000 <vadd_avx2>:
       0:      	shr	rcx, 0x3
       4:      	je	0x23 <vadd_avx2.out>

0000000000000006 <vadd_avx2.loop>:
       6:      	vmovdqu	ymm0, ymmword ptr [rsi]
       a:      	vpaddd	ymm0, ymm0, ymmword ptr [rdx]
       e:      	vmovdqu	ymmword ptr [rdi], ymm0
      12:      	add	rsi, 0x20
      16:      	add	rdx, 0x20
      1a:      	add	rdi, 0x20
      1e:      	dec	rcx
      21:      	jne	0x6 <vadd_avx2.loop>

0000000000000023 <vadd_avx2.out>:
      23:      	vzeroupper
      26:      	ret
$ llvm-mc --triple=riscv64-linux-gnu -mattr=+d,+v -filetype=obj -o vadd_rvv.o riscv/vadd_rvv.s && echo "RVV assembled"
RVV assembled
$ llvm-objdump -d --mattr=+d,+v --no-show-raw-insn vadd_rvv.o | sed -n '/<vadd_rvv>:/,$p'
0000000000000000 <vadd_rvv>:
       0:      	vsetvli	t0, a3, e32, m1, ta, ma
       4:      	vle32.v	v0, (a1)
       8:      	vle32.v	v1, (a2)
       c:      	vadd.vv	v2, v0, v1
      10:      	vse32.v	v2, (a0)
      14:      	sub	a3, a3, t0
      18:      	slli	t1, t0, 0x2
      1c:      	add	a1, a1, t1
      20:      	add	a2, a2, t1
      24:      	add	a0, a0, t1
      28:      	bnez	a3, 0x0 <vadd_rvv>
      2c:      	ret

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 cross_asm_session.py

The read-back shows one shape in all three: load a and b, add, store. Four variants change it. A movdqa version is the same length as movdqu but faults on an address that is not a multiple of 16. An overlapping last vector ends exactly at the array end and rewrites a few lanes the body already wrote. Padding every array to a multiple of four lets the vector body run alone. A saturating add such as uqadd on NEON or paddusb on x86 clips an overflow at the type’s maximum.

Checkpoint

Check: The same loop in SSE2, AVX2 and RVV

  1. Question 1 of 3 Which SSE2 instruction loads 16 bytes into xmm0 from any address, aligned or not?

    Choose one answer.

    Show the answer: Question 1 of 3

    Answer: movdqu

    SSE2 has two 128-bit integer moves: movdqu for any address and movdqa for a 16-byte-aligned one. The vadd above uses movdqu so the loop works whatever alignment the caller passes.

  2. Question 2 of 3 Why does the AVX2 vadd end with vzeroupper?

    Choose one answer.

    Show the answer: Question 2 of 3

    Answer: Intel recommends it when AVX code meets legacy SSE code, to avoid a false dependency the CPU would otherwise carry across the two encodings

    Zeroing the upper halves before legacy SSE code runs removes the dependency the CPU would otherwise carry from the last AVX write of a register to the next legacy SSE write of its low half. The Intel manual states the recommendation; it gives no cycle count.

  3. Question 3 of 3 In the RVV loop, what does vsetvli t0, a3, e32, m1, ta, ma write into t0?

    Choose one answer.

    Show the answer: Question 3 of 3

    Answer: The number of int32 lanes this pass gets, never more than VLMAX for 32-bit elements and LMUL = 1

    vsetvli reads the remaining count, AVL, from a3, sets the vector length vl for the body and writes it to t0. Once AVL is VLMAX or less, vl equals AVL, so the last pass shrinks and the same body handles the tail without a scalar loop.

Practise

Work on a copy of the examples folder and predict before each run.

  1. Change the cases list in vadd.c to {1, 2, 7, 8, 31}, predict the passes and tail columns for each n, then run neon_vadd_session.py; every row must still say yes.
  2. Replace both movdqu loads in x86/vadd_sse2.asm with movdqa, predict whether the offsets of later instructions move, then run cross_asm_session.py.
  3. Delete cbz x4, 2f from neon/vadd.s, work out what subs x4, x4, #1 leaves in x4 when n is 0, then run neon_vadd_session.py: it dies before its first row until the line is back.

Practice that adapts to you

Practise this lesson

7 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 7 A rewrite uses add v2.16b, v0.16b, v1.16b instead of .4s. How many lanes does one pass now add?

    Choose one answer.

    Show the answer: Question 1 of 7

    Answer: 16 lanes of 8 bits each

    The lane count is 128 divided by the lane width: 2 of 64, 4 of 32, 8 of 16 or 16 of 8. The view in the operands names the width.

  2. Question 2 of 7 The vector body uses ld1 {v0.4s}, [x1], #16. What does #16 do after the load?

    Choose one answer.

    Show the answer: Question 2 of 7

    Answer: Adds 16 to x1 so the next iteration reads the next 16 bytes without a separate add

    The post-index form writes the updated base back after each access, so the body is two loads, one add and one store, plus the subs and b.ne of the count-down, with no separate pointer adds.

  3. Question 3 of 7 Why does the function test the tail count with ands x3, x3, #3; b.eq 4f instead of just entering the scalar loop?

    Choose one answer.

    Show the answer: Question 3 of 7

    Answer: For an n that is a multiple of 4, the tail count is 0 and the loop must do nothing; ands sets Z and b.eq skips the body

    The test costs two instructions and lets the scalar body assume a count of at least 1. The recorded n = 4 row is that case: 1 vector pass, 0 tail passes, match yes.

  4. Question 4 of 7 For n = 25, how many scalar tail passes does the vadd run after the vector body?

    Type a number.

    A hint

    The tail runs once for each element the full vectors leave over.

    Show the answer: Question 4 of 7

    Answer: 1 passes

    25 = 6 * 4 + 1, so the vector body runs 6 times and the scalar tail runs once; the pattern is the same as n = 17 in the recorded run.

  5. Question 5 of 7 You port the vadd to 16-bit elements, so one .8h pass adds 8 lanes and the tail runs n % 8 times. Which list of lengths, checked against a scalar reference, reaches every tail count?

    Choose one answer.

    Show the answer: Question 5 of 7

    Answer: Every n from 0 to 16

    A tail of n % 8 passes has eight possible counts, so the test lengths must leave every remainder from 0 to 7, and a length of 0 checks that both loops can be skipped. The recorded run of this page picks n = 0, 3, 4, 10 and 17 for the same reason with four lanes.

  6. Question 6 of 7 The SSE2 loop adds 4 int32 lanes per pass and the AVX2 loop adds 8. Both run on n = 64. How many passes does each loop make?

    Choose one answer.

    Show the answer: Question 6 of 7

    Answer: SSE2 16 passes, AVX2 8 passes

    Passes are n divided by the lanes per pass: 64 / 4 = 16 for 128-bit SSE2 and 64 / 8 = 8 for 256-bit AVX2. The listings compute exactly these counts at the top: shr rcx, 2 in the SSE2 loop and shr rcx, 3 in the AVX2 loop.

  7. Question 7 of 7 The RVV vadd has no scalar tail. What takes its place?

    Choose one answer.

    Show the answer: Question 7 of 7

    Answer: vsetvli returns a smaller t0 on the last pass, and the same body processes those lanes

    vsetvli builds the tail into the vector loop itself. The next lesson covers this vector-length-agnostic style for RVV, SVE and SME together.

Finishing a vector loop safely: tails, overlaps and guard pages Pro: opens with a Pro, Premium or Ultimate pass

The vector body is easy to write; the leftovers are where mistakes live. Three ways to finish a vector loop are compared below, then a lost tail element is reproduced, a vector load faults at a page boundary, and 1,204 lengths and offsets are checked against the scalar loop.

In this part:

  • Scalar tail, overlapping last vector, padded buffer
  • A missing tail stays unwritten
  • A vector load past the end on a guard page
  • 1,204 cases, zero failures

5 runnable examples (C, Python, no_tail.c, vadd_no_tail.s) · about 10 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

SSE2 and AVX2 in depth: the bytes, alignment and vzeroupper Pro: opens with a Pro, Premium or Ultimate pass

The x86-64 section above named movdqa, vzeroupper and the AVX2 VEX form in a sentence each. The listings below show the bytes the assembler produced, the Intel manual’s rules on alignment and on the AVX-to-SSE transition, and the saturating byte add, uqadd on NEON and paddusb on x86.

In this part:

  • The bytes the assembler produced
  • movdqa requires alignment; movdqu does not
  • vzeroupper when AVX code meets legacy SSE
  • Saturating adds: uqadd on NEON and paddusb on x86

3 runnable examples (Python, S) · about 6 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Interview questions

Why does a 128-bit NEON int32 add need a scalar tail when the array length is not a multiple of four?

Each add v2.4s does four 32-bit lanes at once, so a loop of n / 4 passes finishes when the length is 4k. For a length such as 10 or 17 the last 2 or 1 elements fall outside the last vector pass. If the loop stops there they are simply not written: the output buffer holds whatever was there before the call. The usual fix is a short scalar loop for the remaining n % 4 elements. The next lesson covers predicated instructions that handle the tail inside the vector body.

Three deeper questions in the module’s bank build on this one, with a first step and follow-ups for each:

  1. Mid to senior

    A pipeline rewrites a NEON int32 vadd for arrays whose length is 300 to 400 and whose output buffer may be the same as the first input. An interviewer suggests an overlapping last vector to drop the scalar tail. Walk the choice.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Senior to staff

    A build moves from a plain x86-64 target to -mavx2 and the new binary mixes AVX2 code with a hand-written SSE2 routine. A profile shows time inside vzeroupper. Explain what the instruction does and whether the team should cut it.

    Model answer, Pro. First step and follow-ups

Flashcards · 5 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. How many vector iterations does a 128-bit NEON int32 add loop need for n elements?

    n / 4 rounded down, since each add of four 32-bit lanes handles four elements at once.

  2. Why does a NEON int32 loop need a scalar tail when n is not a multiple of 4?

    Each vector pass adds 4 elements, so the vector body stops with n % 4 of them left over. The scalar tail then runs n % 4 times to finish them; without it they would never be written.

  3. Which loop counts must a vector add function test before running its loops?

    Both of them: the number of full vector iterations and the tail count. Either can be zero, and a loop whose count is zero must be skipped.

  4. What does the four-lane int32 loop look like on x86-64 SSE2?

    The same shape as NEON: movdqu loads four int32 values, paddd adds the four lanes, and a scalar loop handles the leftover elements.

  5. Why does the RISC-V V version of the loop need no separate tail?

    vsetvli picks the number of lanes for each iteration, so the last iteration simply gets fewer and the vector loop handles the leftover itself.

Key takeaways

  • A 128-bit NEON int32 add writes four lanes per instruction, so n int32s take n / 4 vector passes.
  • A scalar tail of n % 4 passes handles the leftover elements; the function tests both counts and skips a loop whose count is zero.
  • SSE2 keeps the four-lane shape with movdqu and paddd; AVX2 doubles it to eight lanes with vmovdqu and vpaddd and ends with vzeroupper.
  • RISC-V V picks the lane count per pass with vsetvli, so the vector loop handles the leftover itself.
  • Checking a SIMD routine against a scalar reference, with lengths that leave every remainder from 0 to 3, catches the bugs a reader misses.

Terms in this lesson

loop tail
The leftover iterations of a vector loop that handle fewer elements than one vector pass covers, usually run as a short scalar loop.
scalar reference
A plain non-SIMD implementation of the same function, used to check a vector routine's output lane by lane against an obviously correct one.
unaligned load
A load from any byte address, with no alignment requirement; its aligned counterpart faults when the address is not a multiple of the operand size.
vector loop
A loop body that uses one SIMD instruction to process several lanes of a vector register per pass, in place of several scalar instructions.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.