Rounding seems easy, but it's not.
Floating-point is always harder than you think.
Consider all the corner cases.
std::round(2.5)
But how can this be implemented?
(x + a) - a could ever implement std::roundrint)round?? instructions: all rounding functions except roundround via bit-manipulationtrunc(x ± 0.5) should do it-march=x86-64-v2, trunc becomes a single roundss instruction.float->int conversion (after an input range branch)float round(float x) {
float half = copysign(0.5f, x);
return trunc(x + half);
}
But:
round(0x1p23f ) = 0x1p23fround(0x1p23f+1) = 0x1p23f+2| round(x) | result |
|---|---|
-1.5f |
trunc(-2.0f) |
-1.4f |
trunc(-1.9f) |
-0.5f |
trunc(-1.0f) |
-0.4f |
trunc(-0.9f) |
0.4f |
trunc( 0.9f) |
0.5f |
trunc( 1.0f) |
trunc(x ± 0.5) almost did it, let's adjustfloat round(float x) {
constexpr float almost_half = std::nextafter(0.5f, 0.f);
return trunc(x + copysign(almost_half, x));
}
Now:
round(0x1p23f ) = 0x1p23fround(0x1p23f+1) = 0x1p23f+1round(0.5f) = trunc(0x1p-1f + 0x1.fffffep-2f)trunc(1.f)Only valid for the default rounding mode!
trunc(x ± 0.4999…), why?https://compiler-explorer.com/z/dGPhxMY3T
Clang defaults to -fno-trapping-math
GCC defaults to -ftrapping-math
I believe Clang has the better default.
Anyhow, what do floating-point exceptions have to do with round?
FE_INEXACT — who cares?x + 0x1.fffffep-2f is likely going to be inexact.FE_INEXACT.std::round is specified to not raise any floating-point exceptions except for SNaN.… you can't have nice things.
Or?
roundf?roundf is more efficient than addps; roundssBecause of SIMD.
Let's try a different approach:
return copysign(trunc(abs(x)) + fixup, x);
fixup that will avoid all traps?t = trunc(abs(x));
fixup = abs(x) - t >= .5 ? 1 : 0;
return copysign(t + fixup, x);
This works and doesn't trap.
Except for
abs(x) and t are both NaN <= .5.if (!isinf(x)) {
t = trunc(abs(x));
fixup = abs(x) - t >= .5 ? 1 : 0;
return copysign(t + fixup, x);
} else {
return x;
}
Not bad, but can we go faster and without a branch (for SIMD, anyway)?
https://gcc.gnu.org/bugzilla/show_bug.cgi?id=127119
trunc(abs(x)).Inconclusive. The worst part: the most efficient solution depends on how many elements your input value has and the exact architecture the code will run on. (Also ARM vs. Intel.)
The story is not over yet.
template <__vec_builtin V>
V
__round(V __x)
{
using T = __vec_value_type<V>;
using U = _UInt<sizeof(T)>;
using I = __integer_from<sizeof(T)>;
const auto abs_x = __fabs<_Traits>(__x);
const auto t_abs = __trunc<_Traits>(abs_x);
const auto int_abs_x = __vec_bit_cast<I>(abs_x);
const auto int_t_abs = __vec_bit_cast<I>(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<T>::digits - 1;
constexpr I one_bits = std::bit_cast<I>(T(1));
constexpr I one_half_bits = std::bit_cast<I>(T(.5));
constexpr I bias = one_bits >> mant_width;
const auto biased_exp = int_abs_x >> mant_width;
const auto exponent = __vec_bit_cast<I>(__vec_bit_cast<U>(biased_exp - bias) % numeric_limits<U>::digits);
const auto threshold = ((biased_exp > (mant_width + bias)) | (int_abs_x < one_bits))
? one_half_bits // a) and c)
: (I(1) << (mant_width - 1)) >> exponent; // b)
const V sign_bit = __vec_xor(abs_x, __x);
const V r_abs = t_abs + std::bit_cast<V>(diff < threshold ? 0 : one_bits);
return __vec_or(sign_bit, r_abs);
}