硬件中几乎不支持无符号浮点数,因此您不会拥有这种现成的功能,但您仍然可以通过将最低有效位存储在符号位中来获得非常有效的无符号浮点数。通过这种方式,您可以利用可用的浮点硬件支持,而不是编写软件浮点解决方案。为此,您可以
这是一个简单的 PoC 实现:
#include <cmath>
#include <cfenv>
#include <bit>
#include <type_traits>
// Does the math in double precision when hardware double is available
#define HAS_NATIVE_DOUBLE
class UFloat
{
public:
UFloat(double d) : UFloat(0.0f)
{
if (d < 0)
throw std::range_error("Value must be non-negative!");
uint64_t dbits = std::bit_cast<uint64_t>(d);
bool lsb = dbits & lsbMask;
dbits &= ~lsbMask; // turn off the lsb
d = std::bit_cast<double>(dbits);
value = lsb ? -(float)d : (float)d;
}
UFloat(const UFloat &rhs) : UFloat(rhs.value) {}
// =========== Operators ===========
UFloat &operator+=(const UFloat &rhs)
{
#ifdef HAS_NATIVE_DOUBLE
// Calculate in higher precision then round back
setValue((double)value + rhs.value);
#else
// Calculate the least significant bit manually
bool lhsLsb = std::signbit(value);
bool rhsLsb = std::signbit(rhs.value);
// Clear the sign bit to get the higher significant bits
// then get the sum
value = std::abs(value);
value += std::abs(rhs.value);
if (std::isfinite(value))
{
if (lhsLsb ^ rhsLsb) // Only ONE of the 2 least significant bits is 1
{
// The sum's lsb is 1, so we'll set its sign bit
value = -value;
}
else if (lhsLsb)
{
// BOTH least significant bits are 1s,
// so we'll add the carry to the next bit
value = std::nextafter(value, INFINITY);
// The lsb of the sum is 0, so the sign bit isn't changed
}
}
#endif
return *this;
}
UFloat &operator*=(const UFloat &rhs)
{
#ifdef HAS_NATIVE_DOUBLE
// Calculate in higher precision then round back
setValue((double)value * rhs.value);
#else
// Calculate the least significant bit manually
bool lhsLsb = std::signbit(value);
bool rhsLsb = std::signbit(rhs.value);
// Clear the sign bit to get the higher significant bits
// then get the product
float lhsMsbs = std::abs(value);
float rhsMsbs = std::abs(rhs.value);
// Suppose we have X.xPm with
// X: the high significant bits
// x: the least significant one
// and m: the exponent. Same to Y.yPn
// X.xPm * Y.yPn = (X + 0.x)*2^m * (Y + 0.y)*2^n
// = (X + x/2)*2^m * (Y + y/2)*2^n
// = (X*Y + X*y/2 + Y*x/2 + x*y/4)*2^(m + n)
value = lhsMsbs * rhsMsbs; // X*Y
if (std::isfinite(value))
{
uint32_t rhsMsbsBits = std::bit_cast<uint32_t>(rhsMsb);
value += rhsMsbs*lhsLsb / 2; // X*y/2
uint32_t lhsMsbsBits = std::bit_cast<uint32_t>(lhsMsbs);
value += lhsMsbs*rhsLsb / 2; // Y*x/2
int lsb = (rhsMsbsBits | lhsMsbsBits) & 1; // the product's lsb
lsb += lhsLsb & rhsLsb;
if (lsb & 1)
value = -value; // set the lsb
if (lsb > 1) // carry to the next bit
value = std::nextafter(value, INFINITY);
}
#endif
return *this;
}
UFloat &operator/=(const UFloat &rhs)
{
#ifdef HAS_NATIVE_DOUBLE
// Calculate in higher precision then round back
setValue((double)value / rhs.value);
#else
// Calculate the least significant bit manually
// Do just one more step of long division,
// since we only have 1 bit left to divide
throw std::runtime_error("Not Implemented yet!");
#endif
return *this;
}
double getUnsignedValue() const
{
if (!std::signbit(value))
{
return value;
}
else
{
double result = std::abs(value);
uint64_t doubleValue = std::bit_cast<uint64_t>(result);
doubleValue |= lsbMask; // turn on the least significant bit
result = std::bit_cast<double>(doubleValue);
return result;
}
}
private:
// The unsigned float value, with the least significant bit (lsb)
// being stored in the sign bit
float value;
// the first bit after the normal mantissa bits
static const uint64_t lsbMask = 1ULL << (DBL_MANT_DIG - FLT_MANT_DIG - 1);
// =========== Private Constructor ===========
UFloat(float rhs) : value(rhs)
{
std::fesetround(FE_TOWARDZERO); // We'll round the value ourselves
#ifdef HAS_NATIVE_DOUBLE
static_assert(sizeof(float) < sizeof(double));
#endif
}
void setValue(double d)
{
// get the bit pattern of the double value
auto bits = std::bit_cast<std::uint64_t>(d);
bool lsb = bits & lsbMask;
// turn off the lsb to avoid rounding when converting to float
bits &= ~lsbMask;
d = std::bit_cast<double>(bits);
value = (float)d;
if (lsb)
value = -value;
}
}
可能需要进行更多调整才能获得正确的 lsb
无论哪种方式,您都需要比正常情况更多的操作,因此这可能仅适用于需要考虑缓存占用的大型阵列。在这种情况下,我建议仅将其用作存储格式,就像FP16在大多数当前架构上的处理方式一样:只有加载/存储指令可以扩展为float
或double
转换回。所有算术运算都在float
或double
仅完成
所以无符号浮点数应该只存在于内存中,并且会double
在加载时被解码为完整的。这样您就可以使用本机double
类型,并且不需要在每个运算符之后进行更正
或者,这可以与 SIMD 一起使用以同时对多个无符号浮点数进行操作