diff --git a/include/xsimd/arch/common/xsimd_common_arithmetic.hpp b/include/xsimd/arch/common/xsimd_common_arithmetic.hpp index 0c4d2c437..ad2f0dcee 100644 --- a/include/xsimd/arch/common/xsimd_common_arithmetic.hpp +++ b/include/xsimd/arch/common/xsimd_common_arithmetic.hpp @@ -330,33 +330,69 @@ namespace xsimd } // rotl + // Rotations go through the unsigned type: an arithmetic right shift of a + // negative value would fill the vacated bits with the sign instead of + // the bits shifted out on the left. The runtime count is reduced modulo + // the number of bits, so a count of 0 never shifts by a full lane width. template XSIMD_INLINE batch rotl(batch const& self, STy other, requires_arch) noexcept { + using U = std::make_unsigned_t; constexpr auto bits = std::numeric_limits::digits + std::numeric_limits::is_signed; - return (self << other) | (self >> (bits - other)); + constexpr auto mask = bits - 1; + auto const u = bitwise_cast(self); + if constexpr (std::is_integral_v) + { + auto const s = other & mask; + return bitwise_cast((u << s) | (u >> ((bits - s) & mask))); + } + else + { + auto const m = batch(mask); + auto const o = bitwise_cast(other) & m; + return bitwise_cast((u << o) | (u >> ((batch(bits) - o) & m))); + } } + // A count of 0 reaches bitwise_lshift<0> and bitwise_rshift<0>, which + // return their argument. template XSIMD_INLINE batch rotl(batch const& self, requires_arch) noexcept { + using U = std::make_unsigned_t; constexpr auto bits = std::numeric_limits::digits + std::numeric_limits::is_signed; static_assert(count < bits, "Count amount must be less than the number of bits in T"); - return bitwise_lshift(self) | bitwise_rshift(self); + auto const u = bitwise_cast(self); + return bitwise_cast(bitwise_lshift(u) | bitwise_rshift<(bits - count) % bits>(u)); } // rotr template XSIMD_INLINE batch rotr(batch const& self, STy other, requires_arch) noexcept { + using U = std::make_unsigned_t; constexpr auto bits = std::numeric_limits::digits + std::numeric_limits::is_signed; - return (self >> other) | (self << (bits - other)); + constexpr auto mask = bits - 1; + auto const u = bitwise_cast(self); + if constexpr (std::is_integral_v) + { + auto const s = other & mask; + return bitwise_cast((u >> s) | (u << ((bits - s) & mask))); + } + else + { + auto const m = batch(mask); + auto const o = bitwise_cast(other) & m; + return bitwise_cast((u >> o) | (u << ((batch(bits) - o) & m))); + } } template XSIMD_INLINE batch rotr(batch const& self, requires_arch) noexcept { + using U = std::make_unsigned_t; constexpr auto bits = std::numeric_limits::digits + std::numeric_limits::is_signed; static_assert(count < bits, "Count must be less than the number of bits in T"); - return bitwise_rshift(self) | bitwise_lshift(self); + auto const u = bitwise_cast(self); + return bitwise_cast(bitwise_rshift(u) | bitwise_lshift<(bits - count) % bits>(u)); } // sadd diff --git a/include/xsimd/arch/xsimd_avx2.hpp b/include/xsimd/arch/xsimd_avx2.hpp index ba6825cb8..fd28c524c 100644 --- a/include/xsimd/arch/xsimd_avx2.hpp +++ b/include/xsimd/arch/xsimd_avx2.hpp @@ -396,14 +396,12 @@ namespace xsimd { if constexpr (sizeof(T) == 1) { - __m256i sign_mask = _mm256_set1_epi16((0xFF00 >> shift) & 0x00FF); - __m256i cmp_is_negative = _mm256_cmpgt_epi8(_mm256_setzero_si256(), self); - __m256i res = _mm256_srai_epi16(self, shift); - return _mm256_or_si256( - detail::fwd_to_sse([](__m128i s, __m128i o) noexcept - { return bitwise_and(batch(s), batch(o), sse4_2 {}); }, - sign_mask, cmp_is_negative), - _mm256_andnot_si256(sign_mask, res)); + // Shift 16-bit lanes, remove neighbouring bits, then sign-extend each byte. + constexpr uint8_t keep = static_cast(0xFFu >> shift); + constexpr uint8_t sign = static_cast(0x80u >> shift); + __m256i shifted = _mm256_and_si256(_mm256_srli_epi16(self, static_cast(shift)), _mm256_set1_epi8(static_cast(keep))); + __m256i sign_bit = _mm256_set1_epi8(static_cast(sign)); + return _mm256_sub_epi8(_mm256_xor_si256(shifted, sign_bit), sign_bit); } else if constexpr (sizeof(T) == 2) { @@ -422,10 +420,9 @@ namespace xsimd { if constexpr (sizeof(T) == 1) { - // 8-bit left shift via 16-bit shift + mask + // 8-bit right shift via 16-bit shift + mask of the bits that stay in the byte const __m256i shifted = _mm256_srli_epi16(self, shift); - // TODO(C++17): without `if constexpr ` we must ensure the compile-time shift does not overflow - constexpr uint8_t mask8 = static_cast(sizeof(T) == 1 ? ((1u << shift) - 1u) : 0); + constexpr uint8_t mask8 = static_cast(0xFFu >> shift); const __m256i mask = _mm256_set1_epi8(mask8); return _mm256_and_si256(shifted, mask); } diff --git a/include/xsimd/arch/xsimd_scalar.hpp b/include/xsimd/arch/xsimd_scalar.hpp index 6adc9dce5..d9c76ee2f 100644 --- a/include/xsimd/arch/xsimd_scalar.hpp +++ b/include/xsimd/arch/xsimd_scalar.hpp @@ -418,12 +418,26 @@ namespace xsimd return 1. / x; } + // Rotations go through the unsigned type: an arithmetic right shift of a + // negative value would fill the vacated bits with the sign instead of the + // bits shifted out on the left. The shift is reduced modulo the number of + // bits in T, so rotl(x, 0) and rotl(x, sizeof(T) * 8) both return x. template XSIMD_INLINE std::enable_if_t && std::is_integral_v, T0> rotl(T0 x, T1 shift) noexcept { - constexpr auto bits = std::numeric_limits::digits + std::numeric_limits::is_signed; - return (x << shift) | (x >> (bits - shift)); + if constexpr (std::is_same_v, bool>) + { + return x; + } + else + { + using U = std::make_unsigned_t; + constexpr unsigned bits = sizeof(T0) * 8; + auto const u = static_cast(x); + auto const s = static_cast(shift) & (bits - 1); + return static_cast(static_cast(u << s) | static_cast(u >> ((bits - s) & (bits - 1)))); + } } template XSIMD_INLINE std::enable_if_t, T> @@ -431,15 +445,25 @@ namespace xsimd { constexpr auto bits = std::numeric_limits::digits + std::numeric_limits::is_signed; static_assert(count < bits, "Count must be less than the number of bits in T"); - return (x << count) | (x >> (bits - count)); + return rotl(x, count); } template XSIMD_INLINE std::enable_if_t && std::is_integral_v, T0> rotr(T0 x, T1 shift) noexcept { - constexpr auto bits = std::numeric_limits::digits + std::numeric_limits::is_signed; - return (x >> shift) | (x << (bits - shift)); + if constexpr (std::is_same_v, bool>) + { + return x; + } + else + { + using U = std::make_unsigned_t; + constexpr unsigned bits = sizeof(T0) * 8; + auto const u = static_cast(x); + auto const s = static_cast(shift) & (bits - 1); + return static_cast(static_cast(u >> s) | static_cast(u << ((bits - s) & (bits - 1)))); + } } template XSIMD_INLINE std::enable_if_t, T> @@ -447,7 +471,7 @@ namespace xsimd { constexpr auto bits = std::numeric_limits::digits + std::numeric_limits::is_signed; static_assert(count < bits, "Count must be less than the number of bits in T"); - return (x >> count) | (x << (bits - count)); + return rotr(x, count); } template diff --git a/include/xsimd/arch/xsimd_sse2.hpp b/include/xsimd/arch/xsimd_sse2.hpp index 58e35254d..26e858d9c 100644 --- a/include/xsimd/arch/xsimd_sse2.hpp +++ b/include/xsimd/arch/xsimd_sse2.hpp @@ -500,12 +500,13 @@ namespace xsimd { if constexpr (sizeof(T) == 1) { - // 8-bit arithmetic right shift via 16-bit shift + sign-extension handling. - __m128i shifted = _mm_srai_epi16(self, static_cast(shift)); - __m128i sign_mask = _mm_set1_epi16(static_cast(0xFF00 >> shift)); - __m128i cmp_negative = _mm_cmpgt_epi8(_mm_setzero_si128(), self); - return _mm_or_si128(_mm_and_si128(sign_mask, cmp_negative), - _mm_andnot_si128(sign_mask, shifted)); + // 8-bit arithmetic right shift: logical shift on 16-bit lanes, drop the + // bits that came from the neighbouring byte, then sign-extend. + constexpr uint8_t keep = static_cast(0xFFu >> shift); + constexpr uint8_t sign = static_cast(0x80u >> shift); + __m128i shifted = _mm_and_si128(_mm_srli_epi16(self, static_cast(shift)), _mm_set1_epi8(static_cast(keep))); + __m128i sign_bit = _mm_set1_epi8(static_cast(sign)); + return _mm_sub_epi8(_mm_xor_si128(shifted, sign_bit), sign_bit); } else if constexpr (sizeof(T) == 2) { @@ -522,10 +523,9 @@ namespace xsimd { if constexpr (sizeof(T) == 1) { - // 8-bit left shift via 16-bit shift + mask + // 8-bit right shift via 16-bit shift + mask of the bits that stay in the byte __m128i shifted = _mm_srli_epi16(self, static_cast(shift)); - // TODO(C++17): without `if constexpr ` we must ensure the compile-time shift does not overflow - constexpr uint8_t mask8 = static_cast(sizeof(T) == 1 ? ((1u << shift) - 1u) : 0); + constexpr uint8_t mask8 = static_cast(0xFFu >> shift); const __m128i mask = _mm_set1_epi8(mask8); return _mm_and_si128(shifted, mask); } diff --git a/include/xsimd/types/xsimd_api.hpp b/include/xsimd/types/xsimd_api.hpp index 4fbb86a99..31a850aef 100644 --- a/include/xsimd/types/xsimd_api.hpp +++ b/include/xsimd/types/xsimd_api.hpp @@ -410,7 +410,14 @@ namespace xsimd XSIMD_INLINE batch bitwise_lshift(batch const& x) noexcept { detail::static_check_supported_config(); - return kernel::bitwise_lshift(x, A {}); + if constexpr (shift == 0) + { + return x; + } + else + { + return kernel::bitwise_lshift(x, A {}); + } } template XSIMD_INLINE batch bitwise_lshift(batch const& x, batch const& shift) noexcept @@ -508,7 +515,14 @@ namespace xsimd XSIMD_INLINE batch bitwise_rshift(batch const& x) noexcept { detail::static_check_supported_config(); - return kernel::bitwise_rshift(x, A {}); + if constexpr (shift == 0) + { + return x; + } + else + { + return kernel::bitwise_rshift(x, A {}); + } } /** @@ -2209,7 +2223,8 @@ namespace xsimd * @ingroup batch_bitwise * * Perform a bitwise shift to the left, reintroducing the shifted out bits - * to the right + * to the right. The shift is reduced modulo the number of bits of an + * element. * @param x batch to rotate * @param shift scalar amount to shift * @return rotated \c x. @@ -2237,7 +2252,8 @@ namespace xsimd * @ingroup batch_bitwise * * Perform a bitwise shift to the right, reintroducing the shifted out bits - * to the left. + * to the left. The shift is reduced modulo the number of bits of an + * element. * @param x batch to rotate * @param shift scalar amount to shift * @return rotated \c x. diff --git a/test/test_xsimd_api.cpp b/test/test_xsimd_api.cpp index bc8d8f6bd..0458965fa 100644 --- a/test/test_xsimd_api.cpp +++ b/test/test_xsimd_api.cpp @@ -14,6 +14,10 @@ #include +#include +#include +#include + template struct scalar_type { @@ -519,6 +523,180 @@ TEST_CASE_TEMPLATE("[xsimd api | ssub at type minimum]", B, INTEGRAL_TYPES) } } +template +struct lane_count : std::integral_constant +{ +}; +template +struct lane_count> : std::integral_constant::size> +{ +}; + +template +B load_lanes(typename scalar_type::type const* data) +{ + if constexpr (std::is_integral_v) + { + return *data; + } + else + { + return B::load_unaligned(data); + } +} + +template +T lane(T const& value, size_t) { return value; } + +template +T lane(xsimd::batch const& value, size_t i) { return value.get(i); } + +// Rotating a negative value must reintroduce the bits shifted out, not copies +// of the sign bit. The expected values are computed on the unsigned type, for +// every lane. A count is reduced modulo the number of bits, so counts of 0 and +// of the lane width or more are checked as well. +TEST_CASE_TEMPLATE("[xsimd api | rotations of the sign bit]", B, INTEGRAL_TYPES) +{ + using value_type = typename scalar_type::type; + using U = std::make_unsigned_t; + constexpr int bits = sizeof(value_type) * 8; + constexpr size_t lanes = lane_count::value; + auto ref_rotl = [](U u, int n) + { + constexpr int width = sizeof(value_type) * 8; + n %= width; + return n == 0 ? static_cast(u) : static_cast(static_cast(static_cast(u << n) | static_cast(u >> (width - n)))); + }; + auto ref_rotr = [](U u, int n) + { + constexpr int width = sizeof(value_type) * 8; + n %= width; + return n == 0 ? static_cast(u) : static_cast(static_cast(static_cast(u >> n) | static_cast(u << (width - n)))); + }; + + // 1 followed by zeros, a pattern with the sign bit and the low bit set, + // and a positive pattern. Lane 0 holds the pattern itself, the other + // lanes hold different values derived from it. + U const patterns[] = { static_cast(U(1) << (bits - 1)), static_cast((U(1) << (bits - 1)) | U(1)), static_cast(0x6B) }; + int const counts[] = { 0, 1, bits - 1, bits, bits + 1, 2 * bits + 3 }; + for (U pattern : patterns) + { + std::array in, shifts; + for (size_t i = 0; i < lanes; ++i) + { + in[i] = static_cast(static_cast(pattern ^ static_cast(static_cast(i) * static_cast(0x9E37)))); + shifts[i] = static_cast(counts[(i + pattern) % 6]); + } + B const v = load_lanes(in.data()); + + auto check = [&](B const& left, B const& right, auto left_count, auto right_count) + { + for (size_t i = 0; i < lanes; ++i) + { + CHECK_EQ(lane(left, i), ref_rotl(static_cast(in[i]), left_count(i))); + CHECK_EQ(lane(right, i), ref_rotr(static_cast(in[i]), right_count(i))); + } + }; + + // fixed counts + auto check_fixed = [&](auto count) + { + constexpr int k = decltype(count)::value; + auto const same = [](size_t) + { return k; }; + check(xsimd::rotl(v), xsimd::rotr(v), same, same); + }; + check_fixed(std::integral_constant()); + check_fixed(std::integral_constant()); + check_fixed(std::integral_constant()); + if constexpr (!std::is_integral_v) + { + CHECK_EQ(extract(xsimd::bitwise_rshift<0>(v)), in[0]); + } + + // the same count in every lane + for (int k : { 0, 1, 3, bits - 1, bits, bits + 1, 2 * bits + 3 }) + { + auto const same = [k](size_t) + { return k; }; + check(xsimd::rotl(v, k), xsimd::rotr(v, k), same, same); + } + + // a different count in every lane + B const by = load_lanes(shifts.data()); + auto const per_lane = [&](size_t i) + { return static_cast(shifts[i]); }; + check(xsimd::rotl(v, by), xsimd::rotr(v, by), per_lane, per_lane); + } +} + +TEST_CASE("[xsimd api | boolean rotations]") +{ + for (bool value : { false, true }) + { + CHECK_EQ(xsimd::rotl<0>(value), value); + CHECK_EQ(xsimd::rotr<0>(value), value); + CHECK_EQ(xsimd::rotl(value, 0), value); + CHECK_EQ(xsimd::rotr(value, 0), value); + CHECK_EQ(xsimd::rotl(value, 0), value); + CHECK_EQ(xsimd::rotr(value, 0), value); + } +} + +#ifndef XSIMD_NO_SUPPORTED_ARCHITECTURE +TEST_CASE_TEMPLATE("[xsimd api | fixed shifts by zero]", T, int8_t, uint8_t, int16_t, uint16_t, int32_t, uint32_t, int64_t, uint64_t) +{ + using B = xsimd::batch; + using U = std::make_unsigned_t; + std::array in, left, right; + for (size_t i = 0; i < B::size; ++i) + { + in[i] = static_cast(i % 2 ? U(i + 1) : ~U(i + 1)); + } + B const v = B::load_unaligned(in.data()); + xsimd::bitwise_lshift<0>(v).store_unaligned(left.data()); + xsimd::bitwise_rshift<0>(v).store_unaligned(right.data()); + for (size_t i = 0; i < B::size; ++i) + { + CHECK_EQ(left[i], in[i]); + CHECK_EQ(right[i], in[i]); + } +} + +// The 8-bit fixed right shift is built from a 16-bit shift and a mask that +// removes the bits coming from the neighbouring byte. +template +void check_fixed_rshift_8bit(std::index_sequence) +{ + using B = xsimd::batch; + std::array in, out; + for (int base = 0; base < 256; ++base) + { + for (size_t i = 0; i < B::size; ++i) + { + in[i] = static_cast(base + 37 * i); + } + B const v = B::load_unaligned(in.data()); + auto check = [&](auto shift_tag) + { + constexpr size_t shift = decltype(shift_tag)::value; + xsimd::bitwise_rshift(v).store_unaligned(out.data()); + for (size_t i = 0; i < B::size; ++i) + { + CHECK_EQ(out[i], static_cast(in[i] >> shift)); + } + }; + (check(std::integral_constant {}), ...); + } +} + +TEST_CASE("[xsimd api | fixed right shift of 8-bit integers]") +{ + check_fixed_rshift_8bit(std::make_index_sequence<8> {}); + check_fixed_rshift_8bit(std::make_index_sequence<8> {}); +} +#endif + /* * Functions that apply on floating points types only */