On Sat, Sep 12, 2026 at 6:04 PM Jakub Jelinek <[email protected]> wrote:
>
> On Sat, Sep 12, 2026 at 05:22:47PM +0800, Yuao Ma wrote:
> > Jakub recently implemented most of P3793R2; this patch completes it by
> > implementing the [simd] support and defining the FTM.
> >
> > The implementation is straightforward within the existing SIMD
> > framework. Built and tested on x86_64-linux. OK for trunk?
>
> I admit I know next to nothing about the libstdc++ simd implementation,
> so will defer to Matthias and Jonathan, but I wonder if at least
> for the case of scalar __s it shouldn't be implemented pretty much
> the same as the scalar std::shl/std::shr, i.e. conditionals on
> the __s value vs. number of bits in the element of the _Vp vector
> resp. 0 and doing << or >> (i.e. vector shift by scalar).
> So, if it is called with constant __s, it will be optimized into
> either left or right shift of the vector or 0 or all ones vector
> (or, for the 0 vs. all ones one needs to compare all elements against 0
> if signed, so blend based on the condition). Without relying on the
> compiler SLP optimizing it.
> The shl/shr of vector by vector is harder to do fully vectorized, in
> that case I'd recommend the if constexpr (is_signed_v <...>) and
> probably performing <</>> vector/vector shifts with say masked operand
> and then blending everything together based on the comparisons.
>
> In pure generic vectors (as I said, I really don't know what <simd>
> uses for abstractions), say
> using V = int [[gnu::vector_size (4 * sizeof (int))]];
> V shl (V __x, int __s)
> {
> constexpr auto _Nd = __SIZEOF_INT__ * __CHAR_BIT__;
> if (__s < 0)
> return __s > -_Nd ? __x >> -__s : static_cast<V>(__x < 0);
> return __s < _Nd ? __x << __s : V{};
> }
> (note __x < 0 for GNU vectors results in 0 or all ones elements depending
> on the comparison).
> For the other one roughly
> V shl (V __x, V __s)
> {
> constexpr auto _Nd = __SIZEOF_INT__ * __CHAR_BIT__;
> V __r = __s < _Nd ? __x << (__s & (_Nd - 1)) : V{};
> if constexpr (is_signed_v <decltype(V[0])>)
> __r = __s < 0 ? __s > -_Nd ? __x >> ((-__s) & (_Nd - 1)) :
> static_cast<V>(__x < 0) : __r;
> return __r;
> }
> The latter is gimplified into
> _Nd = 32;
> _1 = __s <= { 31, 31, 31, 31 };
> _2 = __s & { 31, 31, 31, 31 };
> _3 = __x << _2;
> __r = VEC_COND_EXPR <_1, _3, { 0, 0, 0, 0 }>;
> _4 = __s < { 0, 0, 0, 0 };
> _5 = __s >= { -31, -31, -31, -31 };
> _6 = -__s;
> _7 = _6 & { 31, 31, 31, 31 };
> _8 = __x >> _7;
> _9 = __x < { 0, 0, 0, 0 };
> _10 = VEC_COND_EXPR <_9, { -1, -1, -1, -1 }, { 0, 0, 0, 0 }>;
> _11 = VEC_COND_EXPR <_5, _8, _10>;
> __r = VEC_COND_EXPR <_4, _11, __r>;
> so there are no conditional jumps, everything is done on vectors here
> (except the _Nd initialization).
>
> I have no idea if one needs to implement this several times in <simd>
> included headers, once generically using lambda as you did for vectors that
> can't use gnu::vector_size (if there is something like that,
> gnu::vector_size doesn't support e.g. number of elements that isn't power of
> two) and once optimized for the generic vectors.
>
> Jakub
>
Thanks for the quick review! On second thought, I agree that the
initial implementation wasn't fully vectorized. The current <simd>
actually provides support similar to your generic vector approach.
Instead of the shl/shr operations in the current patch, we could do
the following:
template<__simd_integral _VX, __simd_integral _VS>
requires (_VX::size() == _VS::size())
&& (sizeof(typename _VX::value_type) == sizeof(typename _VS::value_type))
[[__gnu__::__always_inline__]]
constexpr _VX
shl(const _VX& __v, const _VS& __s0) noexcept
{
using _Tp = typename _VX::value_type;
using _Sp = typename _VS::value_type;
constexpr int _Nd = sizeof(_Tp) * __CHAR_BIT__;
const rebind_t<_Sp, _VX> __s(__s0);
const _VX __sl(__s & _Sp(_Nd - 1));
_VX __r = select(__s < _Sp(_Nd), __v << __sl, _VX());
if constexpr (is_signed_v<_Sp>)
{
const _VX __sr = -__sl & _Tp(_Nd - 1);
if constexpr (is_signed_v<_Tp>)
__r = select(__s < _Sp(0),
select(__s > _Sp(-_Nd), __v >> __sr, __v >>
(_Nd - 1)), __r);
else
__r = select(__s < _Sp(0), select(__s > _Sp(-_Nd), __v >>
__sr, _VX()), __r);
}
return __r;
}
template<__simd_integral _Vp, __signed_or_unsigned_integer _Sp>
[[__gnu__::__always_inline__]]
constexpr _Vp
shl(const _Vp& __v, _Sp __s) noexcept
{
using _Tp = typename _Vp::value_type;
constexpr int _Nd = sizeof(_Tp) * __CHAR_BIT__;
if constexpr (is_signed_v<_Sp>)
if (__s < 0)
{
if constexpr (is_signed_v<_Tp>)
return __v >> int(__s > -_Nd ? -__s : _Nd - 1);
else
return __s > -_Nd ? __v >> int(-__s) : _Vp();
}
return __s < _Nd ? __v << int(__s) : _Vp();
}
I tested across a wide range of test cases, and this approach results
in at most two conditional jumps. In most cases, it also yields lower
instruction counts. (Because SSE2 lacks per-lane variable shifts,
there are minor regressions in a few corner cases, but AVX2 is a clear
win across the board.)
Does this direction look promising? If so, I’ll update the patch accordingly.