Skip to content

Commit 171349b

Browse files
committed
perf(avx/avx2): plain-move lowering for constant-mask load/store
Half-confined constant-mask load_masked/store_masked shapes now emit plain moves instead of vpmaskmov/vmaskmov, which never store-forward on Intel and are microcoded on AMD. AVX (256-bit, xsimd_avx.hpp): - store_masked: an exact-half prefix/suffix is one plain 128-bit store; a boundary-crossing prefix stores the lower half plain and delegates only the upper remainder, so no shape emits vmaskmov. - load_masked: stay in the value domain so half-confined prefixes reach the sse2 kernels (vmovss/vmovq/vmovlps/vmovsd); an exact-half prefix or suffix is one plain 128-bit load. Only boundary-crossing shapes keep the single 256-bit vmaskmov load (1 uop, no hazard). detail::zero_extend_lo uses _mm256_zextps128_ps256 (no instruction). AVX2 int (xsimd_avx2_128.hpp): - Gate the integer kernels on detail::lowers_to_plain_moves and delegate half-confined shapes to the sse2 float/double kernels via an int->same-width-float bitcast, mirroring the float sibling. Non-plain masks keep the native vpmaskmov path. Previously float prefix<2> loads lowered to mask constant + vpmaskmovd + zero-extend copy (integer domain feeding float FMA); now one vmovq. Measured in FINUFFT spread/interp tails (Meteor Lake AVX2, paired A/B): the vpmaskmov+copy form cost 1.5-2% end to end; plain moves are neutral vs hand-rolled narrow loads.
1 parent 4bb4fe4 commit 171349b

2 files changed

Lines changed: 74 additions & 11 deletions

File tree

include/xsimd/arch/xsimd_avx.hpp

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -985,6 +985,19 @@ namespace xsimd
985985
{
986986
return _mm256_insertf128_pd(_mm256_setzero_pd(), hi, 1);
987987
}
988+
989+
// 128-bit batch into the lower half, upper half zero (no instruction)
990+
template <class A, class SrcA>
991+
XSIMD_INLINE batch<float, A> zero_extend_lo(batch<float, SrcA> const& lo) noexcept
992+
{
993+
return _mm256_zextps128_ps256(lo);
994+
}
995+
996+
template <class A, class SrcA>
997+
XSIMD_INLINE batch<double, A> zero_extend_lo(batch<double, SrcA> const& lo) noexcept
998+
{
999+
return _mm256_zextpd128_pd256(lo);
1000+
}
9881001
}
9891002

9901003
// Runtime-mask load (float/double).
@@ -1021,16 +1034,27 @@ namespace xsimd
10211034
template <class A, class T, bool... Values, class Mode, class = std::enable_if_t<std::is_floating_point<T>::value>>
10221035
XSIMD_INLINE batch<T, A> load_masked(T const* mem, batch_bool_constant<T, A, Values...> mask, convert<T>, Mode, requires_arch<avx>) noexcept
10231036
{
1024-
using int_t = as_integer_t<T>;
10251037
constexpr size_t half_size = batch<T, A>::size / 2;
1026-
using half_arch = typename ::xsimd::make_sized_batch_t<T, half_size>::arch_type;
1038+
using half_batch = make_sized_batch_t<T, half_size>;
1039+
using half_arch = typename half_batch::arch_type;
10271040

1028-
// lower 128-bit half
1029-
XSIMD_IF_CONSTEXPR(mask.countl_zero() >= half_size)
1041+
// exactly the lower 128-bit half: one plain load, upper lanes zero
1042+
XSIMD_IF_CONSTEXPR(mask.prefix() == half_size)
10301043
{
1031-
constexpr auto mlo = ::xsimd::detail::lower_half<half_arch>(batch_bool_constant<int_t, A, Values...> {});
1032-
const auto lo = load_masked(reinterpret_cast<int_t const*>(mem), mlo, convert<int_t> {}, Mode {}, half_arch {});
1033-
return bitwise_cast<T>(batch<int_t, A>(_mm256_zextsi128_si256(lo)));
1044+
return detail::zero_extend_lo<A>(half_batch::load(mem, Mode {}));
1045+
}
1046+
// lower 128-bit half: stay in the value domain so the half kernel can
1047+
// lower pure-prefix shapes to plain narrow moves (movss/movlps/movsd)
1048+
else XSIMD_IF_CONSTEXPR(mask.countl_zero() >= half_size)
1049+
{
1050+
constexpr auto mlo = ::xsimd::detail::lower_half<half_arch>(mask);
1051+
const auto lo = load_masked(mem, mlo, convert<T> {}, Mode {}, half_arch {});
1052+
return detail::zero_extend_lo<A>(lo);
1053+
}
1054+
// exactly the upper 128-bit half: one plain load into the upper lanes
1055+
else XSIMD_IF_CONSTEXPR(mask.suffix() == half_size)
1056+
{
1057+
return detail::zero_extend<A>(half_batch::load(mem + half_size, Mode {}));
10341058
}
10351059
// upper 128-bit half
10361060
else XSIMD_IF_CONSTEXPR(mask.countr_zero() >= half_size)
@@ -1076,8 +1100,30 @@ namespace xsimd
10761100
using half_batch = ::xsimd::make_sized_batch_t<T, half_size>;
10771101
using half_arch = typename half_batch::arch_type;
10781102

1103+
// exactly the lower 128-bit half: one plain store
1104+
XSIMD_IF_CONSTEXPR(mask.prefix() == half_size)
1105+
{
1106+
const half_batch lo = detail::lower_half(src);
1107+
lo.store(mem, Mode {});
1108+
}
1109+
// prefix crossing the 128-bit boundary: plain lower half + prefix-masked
1110+
// upper half. Never emits vmaskmov, which does not store-forward.
1111+
else XSIMD_IF_CONSTEXPR(mask.prefix() > half_size && mask.prefix() < batch<T, A>::size)
1112+
{
1113+
const half_batch lo = detail::lower_half(src);
1114+
lo.store(mem, Mode {});
1115+
constexpr auto mhi = ::xsimd::detail::upper_half<half_arch>(mask);
1116+
const half_batch hi = detail::upper_half(src);
1117+
store_masked<half_arch>(mem + half_size, hi, mhi, Mode {}, half_arch {});
1118+
}
1119+
// exactly the upper 128-bit half: one plain store
1120+
else XSIMD_IF_CONSTEXPR(mask.suffix() == half_size)
1121+
{
1122+
const half_batch hi = detail::upper_half(src);
1123+
hi.store(mem + half_size, Mode {});
1124+
}
10791125
// lower 128-bit half
1080-
XSIMD_IF_CONSTEXPR(mask.countl_zero() >= half_size)
1126+
else XSIMD_IF_CONSTEXPR(mask.countl_zero() >= half_size)
10811127
{
10821128
constexpr auto mlo = ::xsimd::detail::lower_half<half_arch>(mask);
10831129
const half_batch lo = detail::lower_half(src);

include/xsimd/arch/xsimd_avx2_128.hpp

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,19 +141,36 @@ namespace xsimd
141141
}
142142
}
143143

144-
// constant masks gain nothing on a single register; forward to runtime
144+
// Masks that lower to plain moves go to the sse2 float/double kernels
145+
// (int bitcast to same-width float); the rest keep the native vpmaskmov.
145146
template <class A, class T, bool... Values, class Mode,
146147
typename = std::enable_if_t<std::is_integral<T>::value && (sizeof(T) == 4 || sizeof(T) == 8)>>
147148
XSIMD_INLINE batch<T, A> load_masked(T const* mem, batch_bool_constant<T, A, Values...> mask, convert<T>, Mode, requires_arch<avx2_128>) noexcept
148149
{
149-
return load_masked(mem, mask.as_batch_bool(), convert<T> {}, Mode {}, avx2_128 {});
150+
XSIMD_IF_CONSTEXPR(detail::lowers_to_plain_moves(mask))
151+
{
152+
using F = std::conditional_t<sizeof(T) == 4, float, double>;
153+
return bitwise_cast<T>(batch<F, A>(load_masked(reinterpret_cast<F const*>(mem), batch_bool_constant<F, A, Values...> {}, convert<F> {}, Mode {}, sse2 {})));
154+
}
155+
else
156+
{
157+
return load_masked(mem, mask.as_batch_bool(), convert<T> {}, Mode {}, avx2_128 {});
158+
}
150159
}
151160

152161
template <class A, class T, bool... Values, class Mode,
153162
typename = std::enable_if_t<std::is_integral<T>::value && (sizeof(T) == 4 || sizeof(T) == 8)>>
154163
XSIMD_INLINE void store_masked(T* mem, batch<T, A> const& src, batch_bool_constant<T, A, Values...> mask, Mode, requires_arch<avx2_128>) noexcept
155164
{
156-
store_masked(mem, src, mask.as_batch_bool(), Mode {}, avx2_128 {});
165+
XSIMD_IF_CONSTEXPR(detail::lowers_to_plain_moves(mask))
166+
{
167+
using F = std::conditional_t<sizeof(T) == 4, float, double>;
168+
store_masked(reinterpret_cast<F*>(mem), bitwise_cast<F>(src), batch_bool_constant<F, A, Values...> {}, Mode {}, sse2 {});
169+
}
170+
else
171+
{
172+
store_masked(mem, src, mask.as_batch_bool(), Mode {}, avx2_128 {});
173+
}
157174
}
158175

159176
template <class A, class T, class Mode>

0 commit comments

Comments
 (0)