Skip to content

Commit a4b9113

Browse files
authored
perf(avx2/avx512): plain-move lowering for int & EVEX constant-mask load/store (#1379)
Follow-up to #1378, extending the constant-mask plain-move lowering from the AVX float kernel to the remaining paths: - avx: add the boundary-crossing prefix branch to the float load (mirrors the store side). The reported f32 K=6-of-8 case now lowers to vmovups + vmovq + vinsertf128 instead of vmaskmovps. - avx2: int32/int64 constant-mask load/store reuse the AVX float kernel via a shared detail::plain_move_{load,store} helper (bitcast to same-width float). - avx2_128: dedup onto the shared helper. - avx512vl_128/256: prefix/suffix shapes lower to plain moves; interior masks keep the EVEX k-masked path; all() (reached only via the avx512f half-split cascade) uses a plain full move. The plain_move helpers carry a static_assert(sizeof(T) == 4 || 8) guarding the same-width float reinterpret. asm-audited with simdref across SSE2/AVX/AVX2/AVX-512 (and Zen2-4 cost data): prefix/suffix masks emit plain narrow moves (no vmaskmov / vpmaskmov / EVEX k-mask); interior masks keep a single masked op. vmaskmovps==vpmaskmovd and vinsertf128==vinserti128 on every uarch, so the float-domain delegation is cost-neutral. All load/store/shuffle tests pass on SSE2, AVX2, AVX-512.
1 parent 171349b commit a4b9113

5 files changed

Lines changed: 109 additions & 37 deletions

File tree

include/xsimd/arch/xsimd_avx.hpp

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#include "../types/xsimd_avx_register.hpp"
1717
#include "../types/xsimd_batch_constant.hpp"
1818

19+
#include <cassert>
1920
#include <complex>
2021
#include <limits>
2122
#include <type_traits>
@@ -1041,6 +1042,8 @@ namespace xsimd
10411042
// exactly the lower 128-bit half: one plain load, upper lanes zero
10421043
XSIMD_IF_CONSTEXPR(mask.prefix() == half_size)
10431044
{
1045+
// cross-check the plain move via countr_one/countl_zero (independent of prefix())
1046+
assert(mask.countr_one() >= half_size && mask.countl_zero() >= half_size && "lower half fully active, upper empty");
10441047
return detail::zero_extend_lo<A>(half_batch::load(mem, Mode {}));
10451048
}
10461049
// lower 128-bit half: stay in the value domain so the half kernel can
@@ -1051,9 +1054,21 @@ namespace xsimd
10511054
const auto lo = load_masked(mem, mlo, convert<T> {}, Mode {}, half_arch {});
10521055
return detail::zero_extend_lo<A>(lo);
10531056
}
1057+
// prefix crossing the 128-bit boundary: plain lower half +
1058+
// prefix-masked upper half (mirrors the store side)
1059+
else XSIMD_IF_CONSTEXPR(mask.prefix() > half_size && mask.prefix() < batch<T, A>::size)
1060+
{
1061+
// the plain lower-half load reads every lower lane, so they must all be active
1062+
assert(mask.countr_one() >= half_size && "plain lower-half load needs the lower half fully active");
1063+
const half_batch lo = half_batch::load(mem, Mode {});
1064+
constexpr auto mhi = ::xsimd::detail::upper_half<half_arch>(mask);
1065+
const half_batch hi = load_masked(mem + half_size, mhi, convert<T> {}, Mode {}, half_arch {});
1066+
return detail::merge_sse(lo.data, hi.data);
1067+
}
10541068
// exactly the upper 128-bit half: one plain load into the upper lanes
10551069
else XSIMD_IF_CONSTEXPR(mask.suffix() == half_size)
10561070
{
1071+
assert(mask.countl_one() >= half_size && mask.countr_zero() >= half_size && "upper half fully active, lower empty");
10571072
return detail::zero_extend<A>(half_batch::load(mem + half_size, Mode {}));
10581073
}
10591074
// upper 128-bit half
@@ -1103,13 +1118,17 @@ namespace xsimd
11031118
// exactly the lower 128-bit half: one plain store
11041119
XSIMD_IF_CONSTEXPR(mask.prefix() == half_size)
11051120
{
1121+
// a plain store writes every lower lane and no upper lane, so the mask
1122+
// must have the lower half fully active and the upper half empty
1123+
assert(mask.countr_one() >= half_size && mask.countl_zero() >= half_size && "lower half fully active, upper empty");
11061124
const half_batch lo = detail::lower_half(src);
11071125
lo.store(mem, Mode {});
11081126
}
11091127
// prefix crossing the 128-bit boundary: plain lower half + prefix-masked
11101128
// upper half. Never emits vmaskmov, which does not store-forward.
11111129
else XSIMD_IF_CONSTEXPR(mask.prefix() > half_size && mask.prefix() < batch<T, A>::size)
11121130
{
1131+
assert(mask.countr_one() >= half_size && "plain lower-half store needs the lower half fully active");
11131132
const half_batch lo = detail::lower_half(src);
11141133
lo.store(mem, Mode {});
11151134
constexpr auto mhi = ::xsimd::detail::upper_half<half_arch>(mask);
@@ -1119,6 +1138,7 @@ namespace xsimd
11191138
// exactly the upper 128-bit half: one plain store
11201139
else XSIMD_IF_CONSTEXPR(mask.suffix() == half_size)
11211140
{
1141+
assert(mask.countl_one() >= half_size && mask.countr_zero() >= half_size && "upper half fully active, lower empty");
11221142
const half_batch hi = detail::upper_half(src);
11231143
hi.store(mem + half_size, Mode {});
11241144
}
@@ -1172,6 +1192,45 @@ namespace xsimd
11721192
}
11731193
}
11741194

1195+
namespace detail
1196+
{
1197+
// Reinterpret a constant-mask 4/8-byte load/store as same-width float
1198+
// and run DstArch's kernel, which lowers prefix/suffix shapes to plain
1199+
// moves. Shared by the int/EVEX 128- and 256-bit archs.
1200+
template <class DstArch, class A, class T, bool... V, class Mode>
1201+
XSIMD_INLINE batch<T, A> plain_move_load(T const* mem, batch_bool_constant<T, A, V...>, convert<T>, Mode) noexcept
1202+
{
1203+
static_assert(sizeof(T) == 4 || sizeof(T) == 8, "plain-move delegation only supports 4/8-byte lanes");
1204+
using F = std::conditional_t<sizeof(T) == 4, float, double>;
1205+
// same-width float has the same lane count, so the V... mask pack and the
1206+
// memory reinterpret line up one-to-one; wrong here would load the wrong lanes
1207+
static_assert(batch<F, A>::size == batch<T, A>::size, "same-width float must preserve lane count");
1208+
static_assert(sizeof...(V) == batch<T, A>::size, "mask pack width must match the batch");
1209+
// the plain-move path emits aligned moves in aligned/stream mode, which fault
1210+
// on a misaligned pointer (the old vmaskmov tolerated it)
1211+
assert((std::is_same<Mode, unaligned_mode>::value || ::xsimd::is_aligned<A>(mem)) && "aligned/stream masked load needs an aligned pointer");
1212+
// qualify: an unqualified call resolves to detail::load_masked (a different
1213+
// helper) under MSVC's two-phase lookup; we want the kernel-level overload
1214+
return bitwise_cast<T>(batch<F, A>(::xsimd::kernel::load_masked(reinterpret_cast<F const*>(mem), batch_bool_constant<F, A, V...> {}, convert<F> {}, Mode {}, DstArch {})));
1215+
}
1216+
1217+
// Re-tag to DstArch so the vector-mask store kernel is used (the AVX
1218+
// store is gated off EVEX k-register archs).
1219+
template <class DstArch, class A, class T, bool... V, class Mode>
1220+
XSIMD_INLINE void plain_move_store(T* mem, batch<T, A> const& src, batch_bool_constant<T, A, V...>, Mode) noexcept
1221+
{
1222+
static_assert(sizeof(T) == 4 || sizeof(T) == 8, "plain-move delegation only supports 4/8-byte lanes");
1223+
using F = std::conditional_t<sizeof(T) == 4, float, double>;
1224+
static_assert(batch<F, A>::size == batch<T, A>::size, "same-width float must preserve lane count");
1225+
static_assert(sizeof...(V) == batch<T, A>::size, "mask pack width must match the batch");
1226+
assert((std::is_same<Mode, unaligned_mode>::value || ::xsimd::is_aligned<A>(mem)) && "aligned/stream masked store needs an aligned pointer");
1227+
const auto fsrc = bitwise_cast<F>(src);
1228+
// qualify: an unqualified call resolves to detail::store_masked (a different
1229+
// helper) under MSVC's two-phase lookup; we want the kernel-level overload
1230+
::xsimd::kernel::store_masked(reinterpret_cast<F*>(mem), batch<F, DstArch>(fsrc.data), batch_bool_constant<F, DstArch, V...> {}, Mode {}, DstArch {});
1231+
}
1232+
}
1233+
11751234
// lt
11761235
template <class A>
11771236
XSIMD_INLINE batch_bool<float, A> lt(batch<float, A> const& self, batch<float, A> const& other, requires_arch<avx>) noexcept

include/xsimd/arch/xsimd_avx2.hpp

Lines changed: 3 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -150,19 +150,14 @@ namespace xsimd
150150
_mm256_maskstore_epi64(reinterpret_cast<long long*>(mem), mask, src);
151151
}
152152
}
153-
154-
XSIMD_INLINE __m256i zero_extend(__m128i hi) noexcept
155-
{
156-
return _mm256_insertf128_si256(_mm256_setzero_si256(), hi, 1);
157-
}
158153
}
159154

160-
// no half-split shortcut for load; forward to runtime
155+
// Constant masks: prefix/suffix shapes lower to plain moves.
161156
template <class A, class T, bool... Values, class Mode>
162157
XSIMD_INLINE std::enable_if_t<std::is_integral<T>::value && (sizeof(T) == 4 || sizeof(T) == 8), batch<T, A>>
163158
load_masked(T const* mem, batch_bool_constant<T, A, Values...> mask, convert<T>, Mode, requires_arch<avx2>) noexcept
164159
{
165-
return load_masked(mem, mask.as_batch_bool(), convert<T> {}, Mode {}, avx2 {});
160+
return detail::plain_move_load<avx>(mem, mask, convert<T> {}, Mode {});
166161
}
167162

168163
template <class A, class T, class Mode>
@@ -176,28 +171,7 @@ namespace xsimd
176171
typename = std::enable_if_t<std::is_integral<T>::value && (sizeof(T) == 4 || sizeof(T) == 8)>>
177172
XSIMD_INLINE void store_masked(T* mem, batch<T, A> const& src, batch_bool_constant<T, A, Values...> mask, Mode, requires_arch<avx2>) noexcept
178173
{
179-
constexpr size_t lanes_per_half = batch<T, A>::size / 2;
180-
using half_batch = ::xsimd::make_sized_batch_t<T, lanes_per_half>;
181-
using half_arch = typename half_batch::arch_type;
182-
183-
// lower 128-bit half
184-
XSIMD_IF_CONSTEXPR(mask.countl_zero() >= lanes_per_half)
185-
{
186-
constexpr auto mlo = ::xsimd::detail::lower_half<half_arch>(mask);
187-
const half_batch lo = detail::lower_half(src);
188-
store_masked<half_arch>(mem, lo, mlo, Mode {}, half_arch {});
189-
}
190-
// upper 128-bit half
191-
else XSIMD_IF_CONSTEXPR(mask.countr_zero() >= lanes_per_half)
192-
{
193-
constexpr auto mhi = ::xsimd::detail::upper_half<half_arch>(mask);
194-
const half_batch hi = detail::upper_half(src);
195-
store_masked<half_arch>(mem + lanes_per_half, hi, mhi, Mode {}, half_arch {});
196-
}
197-
else
198-
{
199-
detail::maskstore(mem, mask.as_batch(), src);
200-
}
174+
detail::plain_move_store<avx>(mem, src, mask, Mode {});
201175
}
202176

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

include/xsimd/arch/xsimd_avx2_128.hpp

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -149,8 +149,7 @@ namespace xsimd
149149
{
150150
XSIMD_IF_CONSTEXPR(detail::lowers_to_plain_moves(mask))
151151
{
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 {})));
152+
return detail::plain_move_load<sse2>(mem, mask, convert<T> {}, Mode {});
154153
}
155154
else
156155
{
@@ -164,8 +163,7 @@ namespace xsimd
164163
{
165164
XSIMD_IF_CONSTEXPR(detail::lowers_to_plain_moves(mask))
166165
{
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 {});
166+
detail::plain_move_store<sse2>(mem, src, mask, Mode {});
169167
}
170168
else
171169
{

include/xsimd/arch/xsimd_avx512vl_128.hpp

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -307,11 +307,20 @@ namespace xsimd
307307
}
308308
}
309309

310+
// Constant masks: prefix/suffix shapes lower to plain moves; interior
311+
// masks keep the EVEX path.
310312
template <class A, class T, bool... V, class Mode,
311313
typename>
312314
XSIMD_INLINE batch<T, A> load_masked(T const* mem, batch_bool_constant<T, A, V...> mask, convert<T>, Mode, requires_arch<avx512vl_128>) noexcept
313315
{
314-
return detail::maskload128(mem, mask.mask(), Mode {});
316+
XSIMD_IF_CONSTEXPR(detail::lowers_to_plain_moves(mask))
317+
{
318+
return detail::plain_move_load<sse2>(mem, mask, convert<T> {}, Mode {});
319+
}
320+
else
321+
{
322+
return detail::maskload128(mem, mask.mask(), Mode {});
323+
}
315324
}
316325

317326
template <class A, class T, class Mode,
@@ -325,7 +334,14 @@ namespace xsimd
325334
typename>
326335
XSIMD_INLINE void store_masked(T* mem, batch<T, A> const& src, batch_bool_constant<T, A, V...> mask, Mode, requires_arch<avx512vl_128>) noexcept
327336
{
328-
detail::maskstore128(mem, src, mask.mask(), Mode {});
337+
XSIMD_IF_CONSTEXPR(detail::lowers_to_plain_moves(mask))
338+
{
339+
detail::plain_move_store<sse2>(mem, src, mask, Mode {});
340+
}
341+
else
342+
{
343+
detail::maskstore128(mem, src, mask.mask(), Mode {});
344+
}
329345
}
330346

331347
template <class A, class T, class Mode,

include/xsimd/arch/xsimd_avx512vl_256.hpp

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -306,11 +306,25 @@ namespace xsimd
306306
}
307307
}
308308

309+
// Constant masks: prefix/suffix shapes lower to plain moves; interior
310+
// masks keep the EVEX path.
309311
template <class A, class T, bool... V, class Mode,
310312
typename = std::enable_if_t<std::is_arithmetic<T>::value && (sizeof(T) == 4 || sizeof(T) == 8)>>
311313
XSIMD_INLINE batch<T, A> load_masked(T const* mem, batch_bool_constant<T, A, V...> mask, convert<T>, Mode, requires_arch<avx512vl_256>) noexcept
312314
{
313-
return detail::maskload256(mem, mask.mask(), Mode {});
315+
// all() reaches here only via the avx512f half-split cascade.
316+
XSIMD_IF_CONSTEXPR(mask.all())
317+
{
318+
return batch<T, A>::load(mem, Mode {});
319+
}
320+
else XSIMD_IF_CONSTEXPR(detail::lowers_to_plain_moves(mask))
321+
{
322+
return detail::plain_move_load<avx>(mem, mask, convert<T> {}, Mode {});
323+
}
324+
else
325+
{
326+
return detail::maskload256(mem, mask.mask(), Mode {});
327+
}
314328
}
315329

316330
template <class A, class T, class Mode,
@@ -324,7 +338,18 @@ namespace xsimd
324338
typename = std::enable_if_t<std::is_arithmetic<T>::value && (sizeof(T) == 4 || sizeof(T) == 8)>>
325339
XSIMD_INLINE void store_masked(T* mem, batch<T, A> const& src, batch_bool_constant<T, A, V...> mask, Mode, requires_arch<avx512vl_256>) noexcept
326340
{
327-
detail::maskstore256(mem, src, mask.mask(), Mode {});
341+
XSIMD_IF_CONSTEXPR(mask.all())
342+
{
343+
src.store(mem, Mode {});
344+
}
345+
else XSIMD_IF_CONSTEXPR(detail::lowers_to_plain_moves(mask))
346+
{
347+
detail::plain_move_store<avx2>(mem, src, mask, Mode {});
348+
}
349+
else
350+
{
351+
detail::maskstore256(mem, src, mask.mask(), Mode {});
352+
}
328353
}
329354

330355
template <class A, class T, class Mode,

0 commit comments

Comments
 (0)