Module 5 Lessons — Multiply, Floating Point, and NEON

Back to the Course 3 syllabus · Practice: Module 5 exercises

This is the DSP bridge module: the integer multiply family, the floating-point register file and its one subtle instruction (fused multiply-add), the NEON vector unit, and the saturating fixed-point operations that connect straight to Course 2’s Q15 filters. The course’s central benchmark — the dot product, four ways — lives here.


1 · Integer multiply, divide, and the accumulate family

Instruction Computes Note
MUL xd, xa, xb low 64 bits of a·b same bits for signed/unsigned (Module 1 §1)
MADD xd, xa, xb, xc xc + xa·xb multiply-accumulate — the DSP primitive; MUL is MADD with xzr
MSUB xd, xa, xb, xc xc − xa·xb gives the remainder idiom below
SMULL/UMULL xd, wa, wb full 32×32→64 widening: no overflow possible
SMULH/UMULH xd, xa, xb high 64 bits of 64×64 with MUL, assembles the full 128-bit product (Exercise 5.1)
SDIV/UDIV xd, xa, xb quotient see quirks

Division quirks worth knowing cold: division sets no flags; divide-by-zero does not trap — it returns 0; and INT_MIN / −1 returns INT_MIN. Any divide-by-zero policy is yours to code. There is no remainder instruction — the idiom is:

    udiv    x2, x0, x1          ; q = n / d
    msub    x3, x2, x1, x0      ; r = n − q·d

(Exercise 3.4 already used it for digit extraction.)

Why the high half matters for fixed point: a Q31×Q31 product is 62 fractional bits in a 64-bit result — the Q31 answer lives in the high word (shifted by one). Hardware that gives you the high half directly (SMULH here, SMMUL on the Cortex-M4) is fixed-point multiplication’s missing piece — Exercise 5.1’s closing question.

2 · Floating point: registers, operations, comparisons

The FP/SIMD register file is separate from the integer file: 32 registers, each 128 bits, with width views — s0 (32-bit float) and d0 (64-bit double) are the low bits of v0:

v0 (128 bits):  ┌──────────────────────────────┐
                │ q0                            │
                │ d0 ────────────┐              │
                │ s0 ──┐         │              │
                └──────┴─────────┴──────────────┘

Formats are IEEE-754 binary32/binary64 (H&H 5.3: sign · exponent+bias · significand-with-hidden-1; Course 1 §3 owns the numerical analysis). The working instruction set:

  • Arithmetic: FADD, FSUB, FMUL, FDIV, FSQRT, FNEG, FABS (in s/d forms).
  • Moves and conversions: FMOV (register↔︎register, and int↔︎FP as bit pattern); FCVT (float↔︎double); SCVTF/UCVTF (int → FP); FCVTZS/FCVTZU (FP → int, round toward zero — the C cast).
  • Comparison: FCMP sets NZCV; NaN compares unordered — it sets V, and makes conditions like b.ge behave surprisingly. Test explicitly when NaN is possible.
  • Loads/stores: ldr s0, […] / ldr d0, […] — same addressing modes as Module 3 §5.
  • Arguments/results ride v0v7; v8v15 are callee-saved in the low 64 bits (Module 4 §2).

Fused multiply-add: one rounding, not two

FMADD d0, d1, d2, d3 computes d3 + d1·d2 with the product kept at full internal precision — a single rounding at the end. FMUL + FADD rounds twice. Near cancellation the difference is total: with a·b ≈ −c, the separate version’s product rounding annihilates the tiny true result; the fused version keeps it. Exercise 5.3 hunts a concrete (a,b,c) and prints both bit patterns — the cheapest possible demonstration that floating-point operations are defined by their rounding, which is Course 1 §3 made physical. Compilers emit FMA freely under -ffast-math-ish flags and by contraction rules — one reason -O2 output can differ in the last bit from naive expectation, and one thing to spot in Exercise 5.2’s autovectorized listing.

3 · NEON: the vector unit

The same v0v31 registers, now viewed as lanes with an arrangement suffix:

Arrangement Lanes Element
v0.16b / v0.8b 16 / 8 byte
v0.8h / v0.4h 8 / 4 halfword (16-bit)
v0.4s / v0.2s 4 / 2 word (32-bit) — 4 floats
v0.2d 2 doubleword — 2 doubles

One instruction applies its operation to every lane: fadd v0.4s, v1.4s, v2.4s is four float adds. The core moves:

  • Load/store: ld1 {v0.4s}, [x0], #16 — a vector of consecutive elements, post-indexed pointer walk (multi-register forms ld1 {v0.4s–v3.4s} fill four vectors; ld2/ld3/ld4 de-interleave — stereo samples, RGB pixels).
  • Arithmetic: add/sub/mul, fmul/fadd, and fmla vd.4s, va.4s, vb.4s — fused multiply-accumulate per lane: the inner loop of every dot product, FIR, and matrix kernel. A by-element form (fmla v0.4s, v1.4s, v2.s[0]) multiplies a whole vector by one scalar — the filter-coefficient shape.
  • Reductions: lanes must eventually collapse: addv (integer sum-across), faddp (pairwise add — two faddps reduce 4 lanes to 1), smaxv/uminv etc. Standard shape: accumulate vector-wise in the loop, reduce once after it.
  • Widening ops: smull/smlal v0.4s, v1.4h, v2.4h — multiply 16-bit lanes into 32-bit accumulators: the overflow-safe Q15 accumulation pattern, and the direct A64 cousin of the Cortex-M4’s packed SMLALD (Module 6 §9).

The dot product, four ways (Exercise 5.2) is where all of this meets the Module 2 measurement discipline. Expectation management, so the reconciliation has teeth: naive speedup says 4 floats/instruction = 4×. Reality inserts two effects — a single vector accumulator makes the loop a dependent fmla chain (latency-bound, Module 2 §4’s ILP lesson: fix with 2–4 interleaved accumulators), and at 4096 elements the streams begin to feel L1/L2 bandwidth. Whether clang’s -O2 autovectorized loop beats your hand NEON usually comes down to whose unrolling and accumulator count is better — read its disassembly before running (identify: vector width used, number of accumulators, tail handling), then let the numbers arbitrate.

4 · Saturating fixed point: the Q15 world

Course 2’s filters run in Q15: 16-bit signed, 1 sign bit + 15 fraction bits, range [−1, +1). The multiply problem: Q15 × Q15 = Q30 in a 32-bit register — two sign bits at the top. To return to Q15 you double (kill the redundant sign bit → Q31) and take the high 16. And because (−1) × (−1) = +1 is unrepresentable, correct fixed-point arithmetic saturates instead of wrapping (Module 1 §4, Exercise 1.5’s lesson, now per-lane):

  • SQDMULHsigned qsaturating doubling multiply, high half: exactly the sequence above, per lane, in one instruction (sqdmulh v0.4h, v1.4h, v2.4h = four Q15 multiplies). SQRDMULH adds rounding before the truncation — less bias, same cost.
  • SQADD/SQSUB saturate additions; the manual alternative (multiply, shift, compare, clamp — Exercise 5.4’s comparison version) takes ~5 instructions per element and is exactly what the hardware folds away.

Exercise 5.4 drives an accumulator past full scale both ways: the wrapped output inverts (max positive → max negative — a click or a scream in audio), the saturated output clips (distorted but bounded). That’s the entire justification for saturating DSP hardware, on the bench in ten lines. The Cortex-M4’s DSP extensions (QADD16, SMLALD — Module 6 §9) and CMSIS-DSP’s arm_dot_prod_q15 are this section’s ideas at firmware scale — Exercise 6.5 reads them side by side with your NEON.

5 · Reading optimized code, and measuring honestly

The two meta-skills this module graduates:

Reading -O2 (Exercise 5.5, on libc’s own strlen/memcpy): production SIMD always has three parts — an alignment prologue (scalar until a vector boundary), the wide loop (often unrolled 2–4×, multiple accumulators — §3’s latency lesson applied), and tail handling (the last <vector-width elements, or the paged-read trick for strings). Once you can name those parts, any optimized listing stops being noise. This is also the Module 6/8 skill of auditing what the compiler did to firmware.

Benchmarking discipline (the Module 2 callout, now under load): pin predictions first; median of ≥10 runs; min and median; note P-vs-E core scheduling; keep the measured kernel and the harness in separate files so the optimizer can’t fold your benchmark into a constant. Add one more for this module: verify the result of every optimized kernel against a scalar reference before timing it — a fast wrong dot product is a special kind of embarrassing.

Where the exercises hook in

Lesson section Exercise
§1 multiply family, high halves 5.1 — multiply-accumulate zoo
§2–3, §5 all of it at once 5.2 — the dot product, four ways
§2 FMA rounding 5.3 — FMA vs. mul-then-add
§4 Q15 saturation 5.4 — saturating Q15 MAC
§5 reading production SIMD 5.5 — read the master’s code

Reference shelf: the Arm ARM’s SIMD&FP chapters for exact per-instruction semantics; H&H 5.3 for FP formats; Course 1 §3 (conditioning, rounding) and §12 (fixed-point DSP) own the theory this module makes tactile; Smith 11–13 and Plantz 16, 19 (multiplication/division, fractional numbers) retell it book-paced.