Before we start, two pieces of shorthand we will lean on constantly:
The figure below draws exactly this picture: a 256-bit shelf sliced into 8 coloured f32 boxes. Look at the yellow double-arrow marking the full width W=256; each box is 32 bits (e), so counting the boxes gives 256/32=8 lanes. Keep this image in mind — every "how many lanes?" problem is just re-counting boxes on this shelf.
256 bits ⇒ AVX/AVX2 ⇒ ymm.
The mnemonic from the parent note: SSE 128 (xmm), AVX 256 (ymm), AVX-512 512 (zmm) — the numbers literally double. 256 sits in the middle, so it is AVX, register name ymm0–ymm15.
Recall Solution 1.2
addps = packed single → operates on all lanes at once.
addss = scalar single → touches only lane 0, ignores the rest.
A 128-bit SSE register holds 128/32=4 floats, so addps is the one that does 4 additions in a single instruction. addss does exactly 1.
Divide W=512 by each element size e:
32512=16(f32),64512=8(f64),8512=64(int8)16512=32(int16),32512=16(int32),64512=8(int64)
Notice: halve the element size ⇒ double the lanes. int8 (8 bits) gives the most lanes (64) because each element is smallest.
Recall Solution 2.2
Full vectors process 8 at a time. Using floor (round down — see the definition above), the number of full iterations is
⌊8100⌋=12(covers 12×8=96 elements).
The leftover tail is the remainder, i.e. mod:
100mod8=100−96=4 elements.
So 12 vector iterations + a 4-element scalar tail loop. The tail exists because 100 is not a multiple of 8.
Recall Solution 2.3
Ideal speedup ≤ lane count =W/e.
SSE f64: 128/64=2 → up to 2×.
AVX f64: 256/64=4 → up to 4×.
Doubles are twice as wide as floats, so you get half the lanes and half the ceiling of the f32 case.
Amdahl's law for the vectorized fraction (see Amdahl's Law):
Speedup=(1−f)+lanesf1=0.1+80.91=0.1+0.11251=0.21251≈4.71×.
Even with 8 lanes, the sticky 10% serial part chops you from 8× down to ~4.7× — barely more than half of the "obvious" answer. The serial fraction dominates once the parallel part is fast.
The smaller ceiling wins: 4.17×109≪2.0×1011, so the loop is memory-bound. You get ~4.17 billion elements/s, roughly 48× below the compute ceiling. Adding more SIMD lanes here does almost nothing — you must feed the bytes faster. This is exactly the parent note's warning that "AVX is not always 8× faster."
Recall Solution 3.3
A single running total creates a dependency chain: iteration k+1's add cannot start until iteration k's add finishes. If one float add has latency L cycles, N adds take ≈N⋅L cycles even though the adder could accept a new op every cycle.
Eight independent lanes give eight independent chains. While lane 0's add is in flight, lanes 1–7 are also adding — the pipeline stays full, so throughput ≈ 1 add/cycle instead of 1 add per L cycles. We pay the expensive cross-lane fold only once, at the end. This is latency-hiding via independent work.
Follow the parent note's fold. The split step is the one to get right: a 256-bit ymm is two stacked 128-bit halves, and you name each half with a specific intrinsic:
__m128 lo = _mm256_castps256_ps128(acc); // lanes 0..3 — a free "reinterpret", no instruction__m128 hi = _mm256_extractf128_ps(acc, 1); // lanes 4..7 — extract the UPPER 128 bits (index 1)
_mm256_castps256_ps128 is a zero-cost cast: it just tells the compiler "treat the low 128 bits as an xmm," emitting no instruction. _mm256_extractf128_ps(acc, 1) is a real instruction that copies the upper 128-bit half (the 1 selects the high half; 0 would re-select the low half) into an xmm. Now both halves are 128-bit __m128 values, and we combine:
s = _mm_add_ps(lo, hi) → 4 sums: {p0+p4,p1+p5,p2+p6,p3+p7}. (1 op)
s = _mm_hadd_ps(s, s) → lane0 = everything =p0+p1+⋯+p7. (3rd op)
Then float total = _mm_cvtss_f32(s); reads lane 0.
Total = 3 reduction ops (1 add + 2 hadd), producing ∑k=07pk in lane 0. The tree halves the count each step (8→4→2→1), which is why it takes log28=3 combining steps.
The figure below draws this fold as a binary tree. Read it top to bottom: the 8 blue circles are the starting partials p0..p7; the first (pink) row is the 4-lane add that pairs pk with pk+4; the yellow row is the first hadd; the single off-white node at the bottom is the total in lane 0. Notice each row has half the nodes of the one above — that halving is exactly why the count of combining steps is log28=3, matching the op count in the solution.
Recall Solution 4.2
_mm256_load_ps needs address % 32 == 0. The C standard only promises that malloc returns memory aligned for any fundamental object type — that guarantee is at leastalignof(max_align_t), which is commonly 16 bytes but is not required to be 32. So a malloc'd buffer is not guaranteed to be 32-byte aligned; its base (and hence &a[i]) may land at any 4-byte-aligned offset (a float is 4 bytes). Wherever that offset is not a multiple of 32, the aligned load faults:
addressmod32=0⇒fault.
Of the 8 possible 4-byte-aligned offsets {0,4,8,12,16,20,24,28} within a 32-byte window, only offset 0 is safe — so 7 of 8 offsets crash. See Cache and Alignment.
Fixes: (a) use _mm256_loadu_ps (unaligned, tolerates any address, equal speed on modern CPUs), or (b) explicitly request 32-byte alignment with aligned_alloc(32, ...) / posix_memalign so the base is guaranteed 32-byte aligned — never rely on malloc happening to over-align.
Recall Solution 4.3
Throughput ∝lanes×clock.
AVX2: 8×3.0=24.0 (lane·GHz).
AVX-512: 16×2.1=33.6 (lane·GHz).
Ratio =33.6/24.0=1.4. AVX-512 wins by 1.4× here — because the doubled lanes (2×) more than offset the 30% clock loss (×0.7): 2×0.7=1.4. The parent note's warning ("bigger isn't always better") bites only when the downclock is steeper than the lane gain — e.g. if AVX-512 dropped below 1.5 GHz, AVX2 would win.
(a) Floating-point addition is not associative (see Floating Point Representation): (x+y)+z can round differently from x+(y+z) because each + rounds its intermediate result to the nearest representable f32. The 8-lane fold groups additions into a tree, changing which intermediates get rounded, so the last bit(s) of the total may differ from the scalar left-to-right order. Neither is "wrong" — they're differently-rounded valid answers.
(b) Because reordering changes results, a standards-conforming compiler must preserve the exact scalar order by default, which forbids the parallel tree. Only when you promise you don't care — -ffast-math (or -fassociative-math) — will Auto-vectorization and the Compiler reorder the additions and emit the SIMD reduction. Fine for graphics/ML; dangerous where bit-exact reproducibility is required.
Recall Solution 5.2
Replace lanes in Amdahl's law by the achievable per-part speedup s=3 (bandwidth-capped):
Speedup=(1−f)+sf1=0.05+30.951=0.05+0.316671=0.366671≈2.73×.
So despite 8 lanes on paper and 95% vectorizable code, the real win is only ~2.7×, because (i) the vector part hits the memory wall at 3×, and (ii) the last 5% serial part still costs. This is the honest, layered answer the parent note keeps pointing at: apply the ceilings in order — lane count first, then the Roofline bandwidth cap to get the achievable per-part speedup, and only then Amdahl for the whole program. Skipping straight to "8×" would have overstated the win by nearly 3×.
Recall Solution 5.3
Throughput =lanes×clock×parallel units (1 op/cycle).
AVX-512 core: 16×3.0×109×1=4.8×1010 elem/s.
GPU: 32×1.4×109×80=3.584×1012 elem/s.
Ratio =3.584×1012/4.8×1010≈74.7×. The GPU wins ~75× at peak. The architectural idea: massive replication of SIMD lanes across many schedulers — SIMT is "SIMD width × huge thread/warp count." The CPU spends silicon on caches and out-of-order logic; the GPU spends it on more lanes. (Real workloads narrow this gap via memory limits and PCIe transfer — the same Amdahl's Law and Memory Bandwidth and the Roofline Model caveats apply.)