Implement std::numeric_limits<qfloat16>

This shall make it more nearly a first-class numeric type; in
particular, I need some of these for testlib's comparing and
formatting of float16 to handle NaNs and infinities sensibly.

Change-Id: Ic894dd0eb3e05653cd7645ab496463e7a884dff8
Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
Reviewed-by: Thiago Macieira <thiago.macieira@intel.com>
This commit is contained in:
Edward Welbourne 2019-02-21 20:54:03 +01:00
parent b21b07877a
commit 2eb849ed51
3 changed files with 215 additions and 2 deletions

View File

@ -1,5 +1,6 @@
/****************************************************************************
**
** Copyright (C) 2019 The Qt Company Ltd.
** Copyright (C) 2016 by Southwest Research Institute (R)
** Contact: http://www.qt-project.org/legal
**
@ -66,6 +67,13 @@ QT_BEGIN_NAMESPACE
class qfloat16
{
struct Wrap
{
// To let our private constructor work, without other code seeing
// ambiguity when constructing from int, double &c.
quint16 b16;
constexpr inline explicit Wrap(int value) : b16(value) {}
};
public:
constexpr inline qfloat16() noexcept : b16(0) {}
inline qfloat16(float f) noexcept;
@ -75,8 +83,20 @@ public:
bool isInf() const noexcept { return ((b16 >> 8) & 0x7e) == 0x7c; }
bool isNaN() const noexcept { return ((b16 >> 8) & 0x7e) == 0x7e; }
bool isFinite() const noexcept { return ((b16 >> 8) & 0x7c) != 0x7c; }
// Support for std::numeric_limits<qfloat16>
static constexpr qfloat16 _limit_epsilon() noexcept { return qfloat16(Wrap(0x1400)); }
static constexpr qfloat16 _limit_min() noexcept { return qfloat16(Wrap(0x400)); }
static constexpr qfloat16 _limit_denorm_min() noexcept { return qfloat16(Wrap(1)); }
static constexpr qfloat16 _limit_max() noexcept { return qfloat16(Wrap(0x7bff)); }
static constexpr qfloat16 _limit_lowest() noexcept { return qfloat16(Wrap(0xfbff)); }
static constexpr qfloat16 _limit_infinity() noexcept { return qfloat16(Wrap(0x7c00)); }
static constexpr qfloat16 _limit_quiet_NaN() noexcept { return qfloat16(Wrap(0x7e00)); }
// Signalling NaN is 0x7f00
inline constexpr bool isNormal() const noexcept
{ return b16 == 0 || ((b16 & 0x7c00) && (b16 & 0x7c00) != 0x7c00); }
private:
quint16 b16;
constexpr inline explicit qfloat16(Wrap nibble) noexcept : b16(nibble.b16) {}
Q_CORE_EXPORT static const quint32 mantissatable[];
Q_CORE_EXPORT static const quint32 exponenttable[];
@ -263,4 +283,55 @@ QT_END_NAMESPACE
Q_DECLARE_METATYPE(qfloat16)
namespace std {
template<>
class numeric_limits<QT_PREPEND_NAMESPACE(qfloat16)> : public numeric_limits<float>
{
public:
/*
Treat quint16 b16 as if it were:
uint S: 1; // b16 >> 15 (sign)
uint E: 5; // (b16 >> 10) & 0x1f (offset exponent)
uint M: 10; // b16 & 0x3ff (adjusted mantissa)
for E == 0: magnitude is M / 2.^{24}
for 0 < E < 31: magnitude is (1. + M / 2.^{10}) * 2.^{E - 15)
for E == 31: not finite
*/
static constexpr int digits = 11;
static constexpr int min_exponent = -13;
static constexpr int max_exponent = 16;
static constexpr int digits10 = 3;
static constexpr int max_digits10 = 5;
static constexpr int min_exponent10 = -4;
static constexpr int max_exponent10 = 4;
static constexpr QT_PREPEND_NAMESPACE(qfloat16) epsilon()
{ return QT_PREPEND_NAMESPACE(qfloat16)::_limit_epsilon(); }
static constexpr QT_PREPEND_NAMESPACE(qfloat16) (min)()
{ return QT_PREPEND_NAMESPACE(qfloat16)::_limit_min(); }
static constexpr QT_PREPEND_NAMESPACE(qfloat16) denorm_min()
{ return QT_PREPEND_NAMESPACE(qfloat16)::_limit_denorm_min(); }
static constexpr QT_PREPEND_NAMESPACE(qfloat16) (max)()
{ return QT_PREPEND_NAMESPACE(qfloat16)::_limit_max(); }
static constexpr QT_PREPEND_NAMESPACE(qfloat16) lowest()
{ return QT_PREPEND_NAMESPACE(qfloat16)::_limit_lowest(); }
static constexpr QT_PREPEND_NAMESPACE(qfloat16) infinity()
{ return QT_PREPEND_NAMESPACE(qfloat16)::_limit_infinity(); }
static constexpr QT_PREPEND_NAMESPACE(qfloat16) quiet_NaN()
{ return QT_PREPEND_NAMESPACE(qfloat16)::_limit_quiet_NaN(); }
};
template<> class numeric_limits<const QT_PREPEND_NAMESPACE(qfloat16)>
: public numeric_limits<QT_PREPEND_NAMESPACE(qfloat16)> {};
template<> class numeric_limits<volatile QT_PREPEND_NAMESPACE(qfloat16)>
: public numeric_limits<QT_PREPEND_NAMESPACE(qfloat16)> {};
template<> class numeric_limits<const volatile QT_PREPEND_NAMESPACE(qfloat16)>
: public numeric_limits<QT_PREPEND_NAMESPACE(qfloat16)> {};
// Adding overloads to std isn't allowed, so we can't extend this to support
// for fpclassify(), isnormal() &c. (which, furthermore, are macros on MinGW).
} // namespace std
#endif // QFLOAT16_H

View File

@ -766,8 +766,8 @@ static void writeDoubleToCbor(QCborStreamWriter &writer, double d, QCborValue::E
if (qt_is_nan(d)) {
if (opt & QCborValue::UseFloat16) {
if ((opt & QCborValue::UseFloat16) == QCborValue::UseFloat16)
return writer.append(qfloat16(qt_qnan()));
return writer.append(float(qt_qnan()));
return writer.append(std::numeric_limits<qfloat16>::quiet_NaN());
return writer.append(std::numeric_limits<float>::quiet_NaN());
}
return writer.append(qt_qnan());
}

View File

@ -1,5 +1,6 @@
/****************************************************************************
**
** Copyright (C) 2019 The Qt Company Ltd.
** Copyright (C) 2016 by Southwest Research Institute (R)
** Contact: https://www.qt.io/licensing/
**
@ -48,6 +49,7 @@ private slots:
void arithOps();
void floatToFloat16();
void floatFromFloat16();
void limits();
};
void tst_qfloat16::fuzzyCompare_data()
@ -345,5 +347,145 @@ void tst_qfloat16::floatFromFloat16()
QCOMPARE(out[i], expected[i]);
}
static qfloat16 powf16(qfloat16 base, int raise)
{
const qfloat16 one(1.f);
if (raise < 0) {
raise = -raise;
base = one / base;
}
qfloat16 answer = (raise & 1) ? base : one;
while (raise > 0) {
raise >>= 1;
base *= base;
if (raise & 1)
answer *= base;
}
return answer;
}
void tst_qfloat16::limits()
{
// *NOT* using QCOMPARE() on finite qfloat16 values, since that uses fuzzy
// comparison, and we need exact here.
using Bounds = std::numeric_limits<qfloat16>;
QVERIFY(Bounds::is_specialized);
QVERIFY(Bounds::is_signed);
QVERIFY(!Bounds::is_integer);
QVERIFY(!Bounds::is_exact);
QVERIFY(Bounds::is_iec559);
QVERIFY(Bounds::is_bounded);
QVERIFY(!Bounds::is_modulo);
QVERIFY(!Bounds::traps);
QVERIFY(Bounds::has_infinity);
QVERIFY(Bounds::has_quiet_NaN);
QVERIFY(Bounds::has_signaling_NaN);
QCOMPARE(Bounds::has_denorm, std::denorm_present);
QCOMPARE(Bounds::round_style, std::round_to_nearest);
QCOMPARE(Bounds::radix, 2);
// Untested: has_denorm_loss
// A few common values:
const qfloat16 zero(0), one(1), ten(10);
QVERIFY(qIsFinite(zero));
QVERIFY(!qIsInf(zero));
QVERIFY(!qIsNaN(zero));
QVERIFY(qIsFinite(one));
QVERIFY(!qIsInf(one));
QVERIFY(!qIsNaN(one));
QVERIFY(qIsFinite(ten));
QVERIFY(!qIsInf(ten));
QVERIFY(!qIsNaN(ten));
// digits in the mantissa, including the implicit 1 before the binary dot at its left:
QVERIFY(qfloat16(1 << (Bounds::digits - 1)) + one > qfloat16(1 << (Bounds::digits - 1)));
QVERIFY(qfloat16(1 << Bounds::digits) + one == qfloat16(1 << Bounds::digits));
// There is a wilful of-by-one in how m(ax|in)_exponent are defined; they're
// the lowest and highest n for which radix^{n-1} are normal and finite.
const qfloat16 two(Bounds::radix);
qfloat16 bit = powf16(two, Bounds::max_exponent - 1);
QVERIFY(qIsFinite(bit));
QVERIFY(qIsInf(bit * two));
bit = powf16(two, Bounds::min_exponent - 1);
QVERIFY(bit.isNormal());
QVERIFY(!(bit / two).isNormal());
QVERIFY(bit / two > zero);
// Base ten (with no matching off-by-one idiocy):
// the lowest negative number n such that 10^n is a valid normalized value
qfloat16 low10(powf16(ten, Bounds::min_exponent10));
QVERIFY(low10 > zero);
QVERIFY(low10.isNormal());
low10 /= ten;
QVERIFY(low10 == zero || !low10.isNormal());
// the largest positive number n such that 10^n is a representable finite value
qfloat16 high10(powf16(ten, Bounds::max_exponent10));
QVERIFY(high10 > zero);
QVERIFY(qIsFinite(high10));
QVERIFY(!qIsFinite(high10 * ten));
// How many digits are significant ? (Casts avoid linker errors ...)
QCOMPARE(int(Bounds::digits10), 3); // 9.79e-4 has enough sigificant digits:
qfloat16 below(9.785e-4f), above(9.794e-4f);
#if 0 // Sadly, the QEMU x-compile for arm64 "optimises" comparisons:
const bool overOptimised = false;
#else
const bool overOptimised = (below != above);
if (overOptimised)
QEXPECT_FAIL("", "Over-optimised on QEMU", Continue);
#endif // (but it did, so should, pass everywhere else, confirming digits10 is indeed 3).
QVERIFY(below == above);
QCOMPARE(int(Bounds::max_digits10), 5); // we need 5 to distinguish these two:
QVERIFY(qfloat16(1000.5f) != qfloat16(1001.4f));
// Actual limiting values of the type:
const qfloat16 rose(one + Bounds::epsilon());
QVERIFY(rose > one);
if (overOptimised)
QEXPECT_FAIL("", "Over-optimised on QEMU", Continue);
QVERIFY(one + Bounds::epsilon() / rose == one);
QVERIFY(qIsInf(Bounds::infinity()));
QVERIFY(!qIsNaN(Bounds::infinity()));
QVERIFY(!qIsFinite(Bounds::infinity()));
// QCOMPARE(Bounds::infinity(), Bounds::infinity());
QVERIFY(Bounds::infinity() > -Bounds::infinity());
QVERIFY(Bounds::infinity() > zero);
QVERIFY(qIsInf(-Bounds::infinity()));
QVERIFY(!qIsNaN(-Bounds::infinity()));
QVERIFY(!qIsFinite(-Bounds::infinity()));
// QCOMPARE(-Bounds::infinity(), -Bounds::infinity());
QVERIFY(-Bounds::infinity() < zero);
QVERIFY(qIsNaN(Bounds::quiet_NaN()));
QVERIFY(!qIsInf(Bounds::quiet_NaN()));
QVERIFY(!qIsFinite(Bounds::quiet_NaN()));
QVERIFY(!(Bounds::quiet_NaN() == Bounds::quiet_NaN()));
// QCOMPARE(Bounds::quiet_NaN(), Bounds::quiet_NaN());
QVERIFY(Bounds::max() > zero);
QVERIFY(qIsFinite(Bounds::max()));
QVERIFY(!qIsInf(Bounds::max()));
QVERIFY(!qIsNaN(Bounds::max()));
QVERIFY(qIsInf(Bounds::max() * rose));
QVERIFY(Bounds::lowest() < zero);
QVERIFY(qIsFinite(Bounds::lowest()));
QVERIFY(!qIsInf(Bounds::lowest()));
QVERIFY(!qIsNaN(Bounds::lowest()));
QVERIFY(qIsInf(Bounds::lowest() * rose));
QVERIFY(Bounds::min() > zero);
QVERIFY(Bounds::min().isNormal());
QVERIFY(!(Bounds::min() / rose).isNormal());
QVERIFY(qIsFinite(Bounds::min()));
QVERIFY(!qIsInf(Bounds::min()));
QVERIFY(!qIsNaN(Bounds::min()));
QVERIFY(Bounds::denorm_min() > zero);
QVERIFY(!Bounds::denorm_min().isNormal());
QVERIFY(qIsFinite(Bounds::denorm_min()));
QVERIFY(!qIsInf(Bounds::denorm_min()));
QVERIFY(!qIsNaN(Bounds::denorm_min()));
if (overOptimised)
QEXPECT_FAIL("", "Over-optimised on QEMU", Continue);
QCOMPARE(Bounds::denorm_min() / rose, zero);
}
QTEST_APPLESS_MAIN(tst_qfloat16)
#include "tst_qfloat16.moc"