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(ins/dforms). - 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:
FCMPsets NZCV; NaN compares unordered — it sets V, and makes conditions likeb.gebehave surprisingly. Test explicitly when NaN is possible. - Loads/stores:
ldr s0, […]/ldr d0, […]— same addressing modes as Module 3 §5. - Arguments/results ride
v0–v7;v8–v15are 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 v0–v31 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 formsld1 {v0.4s–v3.4s}fill four vectors;ld2/ld3/ld4de-interleave — stereo samples, RGB pixels). - Arithmetic:
add/sub/mul,fmul/fadd, andfmla 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 — twofaddps reduce 4 lanes to 1),smaxv/uminvetc. 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 packedSMLALD(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):
SQDMULH— signed qsaturating doubling multiply, high half: exactly the sequence above, per lane, in one instruction (sqdmulh v0.4h, v1.4h, v2.4h= four Q15 multiplies).SQRDMULHadds rounding before the truncation — less bias, same cost.SQADD/SQSUBsaturate 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.