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.

Reply via email to