# Optimization: ghost-self trick + r_sum fma fuse - **Author**: Linus - **Predecessor**: variant_float_inlining_SoA_AVX2_polyexp_rsqrt.cpp - **File(s)**: variants/variant_float_inlining_SoA_AVX2_polyexp_rsqrt_loopopt.cpp - **Status**: positive ## What this changes Kill the per-block branchless mask in the j-loop. Before the loop we save `pos[i]`, park it at `1e9`, then broadcast i's real position from the saved copy. The j==i lane now reads like a padding lane (huge distance -> ~1e-37 force), so it self-cancels with no mask. That removes the `m_neq_i`/`m_in_range` cmps, the active-AND, and the two safety-ANDs on `f_x` / `f_y` before the accumulator fmadd. Restore `pos[i]` right after the j-loop so the rest of the simulation sees the real positions. Second change in the same variant: fold the two sqrt-recovery muls and the add for `r_sum = |r_ab| + |r_ab2|` into one mul + one fmadd. The two norms are only used in this one place, so we don't need them as named values. ## Code (relevant snippet) ```cpp // ghost-self trick: park pedestrian i at 1e9 for its own j-loop, broadcasting i's // real position from a saved copy. then the j==i lane reads 1e9 like a padding lane // -> ~1e-37 force, self-cancels with no mask. restored right after the loop. const float saved_pix = pos_x[i]; const float saved_piy = pos_y[i]; const __m256 pix = _mm256_set1_ps(saved_pix); const __m256 piy = _mm256_set1_ps(saved_piy); const __m256 eix = _mm256_set1_ps(e_x[i]); const __m256 eiy = _mm256_set1_ps(e_y[i]); pos_x[i] = 1e9f; pos_y[i] = 1e9f; __m256 acc_x = _mm256_setzero_ps(); __m256 acc_y = _mm256_setzero_ps(); for (int j = 0; j < n_p_pad; j += 8) { // no more j_idx / m_neq_i / m_in_range / active __m256 pjx = _mm256_loadu_ps(&pos_x[j]); __m256 pjy = _mm256_loadu_ps(&pos_y[j]); __m256 sbe_jx = _mm256_loadu_ps(&sb_e_x[j]); __m256 sbe_jy = _mm256_loadu_ps(&sb_e_y[j]); __m256 sb_jsq = _mm256_loadu_ps(&sb_sq[j]); __m256 r_ab_x = _mm256_sub_ps(pix, pjx); __m256 r_ab_y = _mm256_sub_ps(piy, pjy); __m256 r_ab_n2 = _mm256_fmadd_ps(r_ab_x, r_ab_x, _mm256_mul_ps(r_ab_y, r_ab_y)); r_ab_n2 = _mm256_max_ps(r_ab_n2, v_eps_sq); __m256 inv_r_ab_n = _mm256_rsqrt_ps(r_ab_n2); __m256 r_ab2_x = _mm256_sub_ps(r_ab_x, sbe_jx); __m256 r_ab2_y = _mm256_sub_ps(r_ab_y, sbe_jy); __m256 r_ab2_n2 = _mm256_fmadd_ps(r_ab2_x, r_ab2_x, _mm256_mul_ps(r_ab2_y, r_ab2_y)); r_ab2_n2 = _mm256_max_ps(r_ab2_n2, v_eps_sq); __m256 inv_r_ab2_n = _mm256_rsqrt_ps(r_ab2_n2); // r_sum fuse: two recovery muls + add -> one mul + one fmadd __m256 r_sum = _mm256_fmadd_ps(r_ab2_n2, inv_r_ab2_n, _mm256_mul_ps(r_ab_n2, inv_r_ab_n)); // ... force-scaling unchanged ... // no f_x_safe / f_y_safe AND before the accumulator acc_x = _mm256_fmadd_ps(f_x, w_val, acc_x); acc_y = _mm256_fmadd_ps(f_y, w_val, acc_y); } // restore the real positions pos_x[i] = saved_pix; pos_y[i] = saved_piy; ``` ## Benchmark Conditions: N_ITER=1000, -O3 -march=native -flto. ### 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) no avx2 here ## Analysis ILP tricks (j-unroll, i-tiling) didn't help earlier, so the win had to come from fewer FP ops, not from running more in parallel. Ghost-self + r_sum fuse just drop ops. Asm diff (gcc 13.3, -O3 -march=native): ``` vcmpps vandps vfmadd vrsqrtps predecessor: 4 4 33 3 variant: 2 1 33 3 ``` The mask is gone, FMA/rsqrt counts unchanged. The AND that mattered was the one between `f_x` and the accumulator fmadd, since that's on the loop-carried chain. ## Takeaway Less work, same precision. The mask was there to handle j==i but the ghost-self lane handles it for free.