AVX2 implementation
This commit is contained in:
committed by
[Quy Anh] «Elliot» Nguyen
parent
174d167b9a
commit
764f07cbe1
@@ -0,0 +1,77 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user