https://gcc.gnu.org/bugzilla/show_bug.cgi?id=127119
Bug ID: 127119
Summary: add vectorization for 'round' with flag_trapping_math
Product: gcc
Version: 17.0
Status: UNCONFIRMED
Keywords: missed-optimization
Severity: enhancement
Priority: P3
Component: target
Assignee: unassigned at gcc dot gnu.org
Reporter: mkretz at gcc dot gnu.org
CC: crazylht at gmail dot com, hongyuw at gcc dot gnu.org
Target Milestone: ---
Target: x86_64-*-*
As discussed in PR127055, round can be vectorized even with -ftrapping-math.
However in PR127055#c19 I proposed an incorrect solution:
abs_x = fabs(x);
t_abs = trunc(abs_x);
r_abs = t_abs + (abs_x - t_abs >= .5 ? 1 : 0);
return or(xor(abs_x, x), r_abs);
This is wrong at x=inf, because abs_x - t_abs raises FE_INVALID. (I fixed my
unit tests to find this now.)
I then rewrote the 'abs_x - t_abs >= .5' test using integer operations. The
resulting function passes all tests (all fp exceptions match exactly what
scalar std::round does). Even more interesting, according to llvm-mca:
-march= / --mcpu= | __round | current -Ofast
--------------------|-----------|---------------------------
skylake / skylake | 19 cycles | 14 cycles
znver5 / znver5 | 11 cycles | 14 cycles
x86-64-v3 / znver5 | 7 cycles | 8 cycles
This is the C++ implementation (using libstdc++-v3/include/bits/vec_ops.h
helpers):
template <__vec_builtin _TV>
_TV
__round(_TV __x)
{
using _Tp = __vec_value_type<_TV>;
using _Up = _UInt<sizeof(_Tp)>;
using _Ip = __integer_from<sizeof(_Tp)>;
const auto __abs_x = __fabs<_Traits>(__x);
const auto __t_abs = __trunc<_Traits>(__abs_x);
const auto __int_abs_x = __vec_bit_cast<_Ip>(__abs_x);
const auto __int_t_abs = __vec_bit_cast<_Ip>(__t_abs);
// a) for |x| without fractional mantissa bits: __diff = 0
// b) for |x| >= 1: XOR cancels exponent bits and mantissa bits
signifying values >= 1
// If the MSB is at the .5 position we need to add 1 to
the result
// c) for |x| < 1: trunc(|x|) is 0 => so __diff = bit-pattern of |x|
// => __diff needs to be compared against
// a) any positive non-zero integer
// b) integer with 1 bit at .5 position
// c) bit-pattern of .5
const auto __diff = __int_abs_x ^ __int_t_abs;
constexpr int __mant_width = numeric_limits<_Tp>::digits - 1;
constexpr _Ip __one_bits = __builtin_bit_cast(_Ip, _Tp(1));
constexpr _Ip __one_half_bits = __builtin_bit_cast(_Ip, _Tp(.5));
constexpr _Ip __bias = __one_bits >> __mant_width;
const auto __biased_exp = __int_abs_x >> __mant_width;
const auto __exponent =
__vec_bit_cast<_Ip>(__vec_bit_cast<_Up>(__biased_exp - __bias) %
numeric_limits<_Up>::digits);
const auto __threshold
= ((__biased_exp > (__mant_width + __bias)) | (__int_abs_x <
__one_bits))
? __one_half_bits // a) and c)
: (_Ip(1) << (__mant_width - 1)) >> __exponent; // b)
const _TV __sign_bit = __vec_xor(__abs_x, __x);
const _TV __r_abs = __t_abs + __builtin_bit_cast(
_TV, __diff < __threshold ? 0 :
__one_bits);
return __vec_or(__sign_bit, __r_abs);
}
I replaced fp subtract with XOR, doing integer compare against a value computed
in the "free" 8 cycles __trunc needs to compute __t_abs.
The main issue is that `>> __exponent` is a vector shift, which is only really
efficient with AVX2. However, improving vector shifts without AVX2 is again a
separate issue to be resolved.
Now, all that said, the resulting number of instructions is likely too much for
inlining. So I assume we rather want to have such a solution in the library,
right?