是否有一种使用现有float32
和float64
格式存储非负浮点值的有效方法?
想象一下默认的float32
行为允许消极/积极:
val = bytes.readFloat32();
如果不需要负值,是否可以允许更大的正值?
val = bytes.readFloat32() + 0xFFFFFFFF;
编辑:基本上当我知道我只存储正值时,浮点格式可以稍微修改一下,以便为相同数量的位提供更大的范围或精度。
EG。 float32
格式定义为 1位用于标志, 8位用于展示, 23位用于分数
如果我不需要符号位,我们可以为expo 8位, 24位如果分数为相同的32位提供更高的精度?
答案 0 :(得分:3)
浮点数(float32
和float64
)具有明确的符号位。浮点数不存在等值的无符号整数。
因此,没有简单的方法可以将正浮点数的范围加倍。
答案 1 :(得分:2)
不,不是免费的。
您可以使用其他数字表示以多种方式扩展范围/准确度。意图不明确,如果您希望float
或double
的范围和准确性使用其他数字表示(大小相等),性能通常会很差。
坚持使用float
或double
,除非性能/存储非常重要,您可以使用其他数字表示来表示您的价值(或更好!)。
答案 2 :(得分:0)
有almost no support for unsigned float in hardware,因此您将没有现成的功能,但是通过将最低有效位存储在符号位中,您仍然可以具有相当高效的无符号浮点数。这样,您可以利用硬件可用的浮动支持而不是编写软件浮动解决方案。为此,您可以
每次操作后手动对其进行操作
这样,您需要对lsb(又称符号位)进行一些小的校正,例如,再加长1个除法步长,或者对加法使用1位加法器
,或者通过更高的精度进行数学运算(如果可用)
例如,如果类型为float
,则可以在double
中进行操作,然后在存储时回退到float
这是一个简单的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:
// =========== Constructors ===========
UFloat(float rhs = 0.0f) : value(rhs)
{
std::fesetround(FE_TOWARDZERO); // We'll round the value ourselves
#ifdef HAS_NATIVE_DOUBLE
static_assert(sizeof(float) < sizeof(double));
#endif
}
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 least significant bit is 1, so we'll set its sign bit
value = -value;
}
else if (lhsLsb)
{
// Both least significant bits are 1s, so add the carry to the next bit
// The least significant bit of the sum is 0, so we don't change the sign 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
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 (double)value;
}
else
{
double result = std::abs(value);
uint64_t doubleValue;
memcpy(&doubleValue, &result, sizeof result);
doubleValue |= lsbMask; // turn on the least significant bit
memcpy(&result, &doubleValue, sizeof result);
return result;
}
}
private:
// The unsigned float value, with the least significant bit 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);
void setValue(double d)
{
auto bits = std::bit_cast<std::uint64_t>(d); // get the bit pattern of the double value
bool lsb = bits & lsbMask;
bits &= ~lsbMask; // turn off the lsb to avoid rounding when converting to float
d = std::bit_cast<double>(bits);
value = (float)d;
if (lsb)
value = -value;
}
}
可能需要更多调整才能获得正确的lsb
无论哪种方式,您都需要比平常更多的操作,因此这仅对关注缓存占用空间的大型阵列有用。在那种情况下,我建议将此仅用作存储格式,就像在大多数当前体系结构上如何对待FP16一样:只有加载/存储指令可以扩展为{{1 }}或float
并转换回来。所有算术运算仅在double
或float
中完成
因此,无符号浮点数应仅存在于内存中,并且在加载时将被解码为完整的double
。这样,您就可以处理本机double
类型,无需在每个运算符之后进行更正