Files
263-0007-00L-Advanced-Syste…/docs/optimization_inlining_7_SoA_AVX2.md
Linus Baumberger 764f07cbe1 AVX2 implementation
2026-05-25 11:43:58 +02:00

3.1 KiB

Optimization: AVX2 vectorization of the ped-ped loop

  • Author: Linus
  • Predecessor: variant_inlining_7_SoA.cpp
  • File(s): variants/variant_inlining_7_SoA_AVX2.cpp
  • Status: TBD, positive locally (approx.2 times)

What this changes

First AVX2 pass through the ped-ped inner loop. The j loop strides by 4 and processes 4 pedestrians at once on __m256d registers. SoA from the predecessor variant is what makes the loads contiguous, otherwise this would not have been clean to write.

The i == j skip is now a mask instead of continue. I also pad all the per-pedestrian arrays to a multiple of 4 (n_p_pad) so the loop doesn't need a scalar tail. Padding positions are set to 1e9 so even without the mask the lane would contribute ~0 via exp(very negative), but the mask makes it deterministic. Same mask is also used to zero out the i==j lane after the body.

The scalar exp() is still called 4 times per j step into a scratch array.

Code (relevant snippet)

See variants/variant_inlining_7_SoA_AVX2.cpp for the full inner loop. Structure is: build the active mask from j_idx vs i and n_p, run the vector body on 4 lanes at a time, blend in a safe denom so the i==j lane does not produce a NaN, mask the per-lane force contribution, then accumulate into acc_x and acc_y. After the j loop a horizontal sum folds the accumulators into the scalar f_alpha_dt.

Benchmark

Conditions: min of N runs (TBD), N_ITER=1000, -O3 -march=native -flto. Diff is vs variant_inlining_7_SoA.

local runs show a clear speedup over the SoA-only predecessor. exact numbers TBD on the test machine, but the qualitative picture is that the 4 scalar exp() calls inside the j loop now dominate.

x86

Machine: TBD

Frequency: TBD

N W(n) [Flops] T(n) [Cycles] Performance [Flops/Cycle] Diff vs Predecessor
50
100
200
500
1000

ARM (Apple)

Not applicable, this variant uses AVX2 intrinsics

Analysis

The win comes from doing 4 ped-ped interactions per j step instead of 1. Loads are contiguous thanks to SoA, and the inner body is mostly straight-line sub, fmadd, mul, sqrt, one div, plus a blendv and a couple of cmp/and for the mask. No branches in the j loop other than the loop counter.

I expected exp() to become the bottleneck once everything around it was vectorized, and the profile confirmed it. The horizontal sum at the end of the j loop only happens once per outer iter, not per j step, so its cost is small.

Takeaway

First real perf jump for the ped-ped force, but limited by the scalar exp call still inside the loop. Sets up the polyexp variant.