6.1.11 · D3Parallelism & Multicore

Worked examples — Vector - SIMD instructions (SSE, AVX, NEON)

2,524 words11 min readBack to topic

This page is a drill sheet for the parent SIMD note. There we learned that SIMD means one instruction, many data elements at once, and that the speedup for vector width is roughly . Here we stress-test that idea against every awkward case a real program can throw at it — the clean case, the leftover case, the overflow case, the branchy case, and the exam trick.

Before symbols: let me re-anchor two words so nothing below is a mystery.

Figure — Vector - SIMD instructions (SSE, AVX, NEON)

The scenario matrix

Every SIMD problem is really a question about how the data lines up with the tray. Here is the full grid of cases — the six worked examples below each fill one or more cells.

Cell The scenario it tests Where it bites Worked in
C1 is an exact multiple of The "clean" ideal case Ex 1
C2 is not a multiple of (remainder tail) , scalar cleanup Ex 2
C3 (fewer elements than one tray) Degenerate: still costs one instruction Ex 3
C4 (empty input) Limiting case: speedup undefined Ex 3
C5 Reduction (sum across lanes, not element-wise) Horizontal add, log-tree cost Ex 4
C6 Overflow / saturation on small integers must clamp, not wrap Ex 5
C7 Data-dependent branch inside the loop Masking; SIMD may lose Ex 6
C8 Real-world word problem Frame budget for image filter Ex 7
C9 Exam twist: mixed element sizes / alignment Trick numbers, the gotcha Ex 8

Example 1 — C1: The clean multiple

Forecast: Before reading on — jot a number. How many vector adds, and what speedup?

  1. Find . Why this step? The tray size in elements is what divides the work. A 256-bit register 32-bit per float . So .
  2. Count vector instructions. Why? Each instruction chews through one tray. Number exactly — no ceiling rounding because 8000 is a clean multiple of 8.
  3. Speedup. Why? Scalar does adds; SIMD does 1000. So .

Verify: Units check — instructions are dimensionless counts. floats processed, matching . ✓


Example 2 — C2: The remainder tail

Forecast: Guess the cleanup count first — is it 3? Something else?

  1. Full trays. Why? Integer division tells us how many complete groups of 8 fit. full vector adds covering elements.
  2. Remainder. Why this step? Whatever is left can't fill a tray, so it falls to scalar code. leftover elements → 3 scalar adds.
  3. Total instruction-equivalents. Why? To compare fairly we count both parts: vector + scalar "adds" worth of work.
  4. Real speedup. Why? Scalar baseline is adds. .
Figure — Vector - SIMD instructions (SSE, AVX, NEON)

Verify: ✓. And (the ceiling formula) — note it counts the tail as one padded vector op, giving the slightly-optimistic . Both views are consistent; the scalar-tail view (7.98) is the honest one.


Example 3 — C3 & C4: Fewer than a tray, and empty

Forecast: Does 5 elements still cost a whole instruction? Is the speedup above or below 8?

  1. Case (a), instruction count. Why ceiling? You cannot issue "5/8 of an instruction" — the hardware runs one whole addps and simply ignores (or masks) the 3 empty cups. .
  2. Case (a), speedup. Why? Scalar = 5 adds; SIMD = 1. . Below the ceiling of 8 because 3 lanes were wasted.
  3. Case (b), the empty input. Why care? Limiting behaviour: gives instructions. Speedup is undefined — there's nothing to accelerate.

Verify: (a) ✓ (speedup equals width times utilization). (b) Any code should guard and skip the loop entirely; no division by zero occurs because the loop body never runs.


Example 4 — C5: Reduction (sum across lanes)

Forecast: Guess the number of reduction stages: 8? 3? 7?

  1. Why a tree, not a straight line? A vector adds lane-to-lane in parallel, so we fold the 8 values in halves: . Each fold is one vector add.
  2. Count stages. Why ? Halving repeatedly from 8 needs folds. That's the key gain — a scalar loop needs sequential adds.
Figure — Vector - SIMD instructions (SSE, AVX, NEON)
  1. Total AVX op count. Why add the 1? One vmulps produces all 8 products, then 3 reduce-adds. vector ops.
  2. Scalar baseline. Why 15? 8 multiplies + 7 adds scalar ops.
  3. Speedup. — well below because reductions have that unavoidable -tree overhead.
Recall Why reductions never hit full speedup

Question: Why is a dot product only ~4× on 8-wide SIMD instead of 8× ::: Element-wise multiply parallelizes perfectly (8×), but summing across lanes needs extra folding steps that a scalar loop hides inside its single accumulator — so the wins are diluted.

Verify: stages ✓; scalar ops ✓; AVX ops ✓; ✓.


Example 5 — C6: Overflow vs. saturation

Forecast: What does become in 8 bits when it wraps?

  1. The true sum. Why? , which exceeds the 8-bit ceiling of 255.
  2. Wrapping result. Why modulo 256? An 8-bit register holds values ; overflow wraps around by . . A bright pixel turns nearly black — a visible bug.
  3. Saturating result. Why clamp? vqadd (the q = saturating) clamps anything above 255 down to 255. So the pixel becomes 255 (pure white) — the physically sensible answer.

Verify: ; ✓; ✓. Register still holds 16 lanes in one instruction regardless of which add variant is chosen.


Example 6 — C7: The branchy loop

Forecast: Both paths do the same work — but which has fewer total ops? Trust your gut.

  1. Scalar cost. Why 2 per element? One compare, one add-or-sub. Total ops.
  2. SIMD instruction count. Why 4 per tray? SIMD can't branch, so it computes both outcomes and selects: compare, inc-all, dec-all, blend = 4 vector ops per 8 elements. Number of trays . Total vector ops.
  3. Compare fairly. Why note "still wins here"? , so SIMD does win on op-count for this clean, predictable branch.
  4. Why the parent warned anyway. Because SIMD did 2× the useful work (both inc and dec every lane) — utilization is only 50%. If branches are rare or unpredictable, scalar branch prediction can beat this doubled work. The lesson: SIMD-with-masking works, but its speedup is throttled by wasted lanes.

Verify: Scalar ✓; SIMD ✓; ratio (a 4× edge here, but with 50% wasted computation).


Example 7 — C8: Real-world word problem

Forecast: Will the filter eat most of the frame budget, or a tiny sliver?

  1. Pixels per frame. Why multiply? Image is a grid. pixels.
  2. NEON instructions per frame. Why divide by 16? Each vqaddq_u8 brightens 16 pixels. instructions (clean multiple — , no tail).
  3. Instructions per second. Why ? 60 frames each second. instructions/s.
  4. Time per frame. Why? At 1 ns each: .
  5. Fraction of budget. Why 16.67 ms? A 60 fps frame is ms. Fraction .

Verify: ✓; ✓; ✓; ✓. Units: ns/ms cancels to a pure fraction ✓. The filter is nearly free — leaving the budget for decoding and display.


Example 8 — C9: The exam twist (mixed sizes + alignment)

Forecast: The array size and the address are both traps. Spot them before solving.

  1. Find — first trap. Why not 4? Students assume from float examples. But these are 16-bit ints: . So .
  2. Instruction count with tail. Why? full vectors covering elements; remainder scalar ops. So 512 vector + 2 scalar.
  3. Alignment — second trap. Why check the address bits? Aligned loads need the address divisible by 16. in decimal; . Not alignedmovaps would fault. You must use the unaligned load movups (slower) or re-allocate on a 16-byte boundary.

Verify: ✓; , tail ✓; so aligned load illegal ✓.


Where to go next

  • The masking idea in Ex 6 is the heart of Data Paralelism and why Loop Vectorization and Auto-vectorization refuse to touch unpredictable branches.
  • The alignment trap in Ex 8 ties directly into Cache Optimization (64-byte cache lines).
  • SIMD sits in the "SI-MD" box of Flynn's Taxonomy; when the arrays get huge, you graduate to GPU Architecture. When they stay small but independent, Instruction-Level Parallelism picks up the slack.
Recall Self-test

Ideal speedup of clean on 8-wide AVX ::: exactly 8 Vector instructions for , , plus scalar tail ::: 1250 vector + 3 scalar in 8-bit wrapping add ::: 24 with saturating add ::: 255 NEON instructions to brighten a grayscale frame ::: 129600