我需要一个跨平台的库/算法,它将在32位和16位浮点数之间进行转换。我不需要使用16位数进行数学运算;我只需要减小32位浮点数的大小,以便它们可以通过网络发送。我在C ++工作。
我理解我会失去多少精确度,但这对我的应用来说是可以的。
IEEE 16位格式会很棒。
答案 0 :(得分:47)
从单精度到半精度的完全转换。这是我的SSE版本的直接副本,因此它是无分支的。它利用了GCC(-true == ~0)中的事实,对于VisualStudio也可能如此,但我没有副本。
class Float16Compressor
{
union Bits
{
float f;
int32_t si;
uint32_t ui;
};
static int const shift = 13;
static int const shiftSign = 16;
static int32_t const infN = 0x7F800000; // flt32 infinity
static int32_t const maxN = 0x477FE000; // max flt16 normal as a flt32
static int32_t const minN = 0x38800000; // min flt16 normal as a flt32
static int32_t const signN = 0x80000000; // flt32 sign bit
static int32_t const infC = infN >> shift;
static int32_t const nanN = (infC + 1) << shift; // minimum flt16 nan as a flt32
static int32_t const maxC = maxN >> shift;
static int32_t const minC = minN >> shift;
static int32_t const signC = signN >> shiftSign; // flt16 sign bit
static int32_t const mulN = 0x52000000; // (1 << 23) / minN
static int32_t const mulC = 0x33800000; // minN / (1 << (23 - shift))
static int32_t const subC = 0x003FF; // max flt32 subnormal down shifted
static int32_t const norC = 0x00400; // min flt32 normal down shifted
static int32_t const maxD = infC - maxC - 1;
static int32_t const minD = minC - subC - 1;
public:
static uint16_t compress(float value)
{
Bits v, s;
v.f = value;
uint32_t sign = v.si & signN;
v.si ^= sign;
sign >>= shiftSign; // logical shift
s.si = mulN;
s.si = s.f * v.f; // correct subnormals
v.si ^= (s.si ^ v.si) & -(minN > v.si);
v.si ^= (infN ^ v.si) & -((infN > v.si) & (v.si > maxN));
v.si ^= (nanN ^ v.si) & -((nanN > v.si) & (v.si > infN));
v.ui >>= shift; // logical shift
v.si ^= ((v.si - maxD) ^ v.si) & -(v.si > maxC);
v.si ^= ((v.si - minD) ^ v.si) & -(v.si > subC);
return v.ui | sign;
}
static float decompress(uint16_t value)
{
Bits v;
v.ui = value;
int32_t sign = v.si & signC;
v.si ^= sign;
sign <<= shiftSign;
v.si ^= ((v.si + minD) ^ v.si) & -(v.si > subC);
v.si ^= ((v.si + maxD) ^ v.si) & -(v.si > maxC);
Bits s;
s.si = mulC;
s.f *= v.si;
int32_t mask = -(norC > v.si);
v.si <<= shift;
v.si ^= (s.si ^ v.si) & mask;
v.si |= sign;
return v.f;
}
};
因此需要考虑很多因素,它会处理所有次正常值,包括无穷大,静音NaN,信号NaN和负零。当然,并不总是需要完整的IEEE支持。所以压缩泛型浮点数:
class FloatCompressor
{
union Bits
{
float f;
int32_t si;
uint32_t ui;
};
bool hasNegatives;
bool noLoss;
int32_t _maxF;
int32_t _minF;
int32_t _epsF;
int32_t _maxC;
int32_t _zeroC;
int32_t _pDelta;
int32_t _nDelta;
int _shift;
static int32_t const signF = 0x80000000;
static int32_t const absF = ~signF;
public:
FloatCompressor(float min, float epsilon, float max, int precision)
{
// legal values
// min <= 0 < epsilon < max
// 0 <= precision <= 23
_shift = 23 - precision;
Bits v;
v.f = min;
_minF = v.si;
v.f = epsilon;
_epsF = v.si;
v.f = max;
_maxF = v.si;
hasNegatives = _minF < 0;
noLoss = _shift == 0;
int32_t pepsU, nepsU;
if(noLoss) {
nepsU = _epsF;
pepsU = _epsF ^ signF;
_maxC = _maxF ^ signF;
_zeroC = signF;
} else {
nepsU = uint32_t(_epsF ^ signF) >> _shift;
pepsU = uint32_t(_epsF) >> _shift;
_maxC = uint32_t(_maxF) >> _shift;
_zeroC = 0;
}
_pDelta = pepsU - _zeroC - 1;
_nDelta = nepsU - _maxC - 1;
}
float clamp(float value)
{
Bits v;
v.f = value;
int32_t max = _maxF;
if(hasNegatives)
max ^= (_minF ^ _maxF) & -(0 > v.si);
v.si ^= (max ^ v.si) & -(v.si > max);
v.si &= -(_epsF <= (v.si & absF));
return v.f;
}
uint32_t compress(float value)
{
Bits v;
v.f = clamp(value);
if(noLoss)
v.si ^= signF;
else
v.ui >>= _shift;
if(hasNegatives)
v.si ^= ((v.si - _nDelta) ^ v.si) & -(v.si > _maxC);
v.si ^= ((v.si - _pDelta) ^ v.si) & -(v.si > _zeroC);
if(noLoss)
v.si ^= signF;
return v.ui;
}
float decompress(uint32_t value)
{
Bits v;
v.ui = value;
if(noLoss)
v.si ^= signF;
v.si ^= ((v.si + _pDelta) ^ v.si) & -(v.si > _zeroC);
if(hasNegatives)
v.si ^= ((v.si + _nDelta) ^ v.si) & -(v.si > _maxC);
if(noLoss)
v.si ^= signF;
else
v.si <<= _shift;
return v.f;
}
};
这会强制所有值进入可接受的范围,不支持NaN,无穷大或负零。 Epsilon是该范围内允许的最小值。精度是浮点数保留的精度位数。虽然上面有很多分支,但它们都是静态的,并且将由CPU中的分支预测器缓存。
当然,如果您的值不需要对数分辨率接近零。然后,如已经提到的那样,将它们线性化为定点格式要快得多。
我在图形库中使用FloatCompressor(SSE版本)来减小内存中线性浮点颜色值的大小。压缩浮动具有创建小型查找表的优点,用于耗时的功能,例如伽马校正或超越。压缩线性sRGB值减少到最大12位或最大值3011,这对于sRGB的查找表大小非常有用。
答案 1 :(得分:18)
std::frexp
从正常的浮点数或双精度数中提取有效数和指数 - 然后你需要决定如何处理太大而不适合半精度浮点数的指数(饱和......?),相应地调整,并将半精度数放在一起。 This article有C源代码,可向您展示如何执行转换。
答案 2 :(得分:18)
考虑到你的需求(-1000,1000),也许最好使用定点表示法。
//change to 20000 to SHORT_MAX if you don't mind whole numbers
//being turned into fractional ones
const int compact_range = 20000;
short compactFloat(double input) {
return round(input * compact_range / 1000);
}
double expandToFloat(short input) {
return ((double)input) * 1000 / compact_range;
}
这将使您精确到最接近的0.05。如果您将20000更改为SHORT_MAX,您将获得更高的准确性,但是一些整数将在另一端以小数形式结束。
答案 3 :(得分:13)
浮动的一半:
float f = ((h&0x8000)<<16) | (((h&0x7c00)+0x1C000)<<13) | ((h&0x03FF)<<13);
浮到一半:
uint32_t x = *((uint32_t*)&f);
uint16_t h = ((x>>16)&0x8000)|((((x&0x7f800000)-0x38000000)>>13)&0x7c00)|((x>>13)&0x03ff);
答案 4 :(得分:5)
如果你要发送信息流,你可能会做得比这更好,特别是如果一切都在一致的范围内,就像你的应用程序似乎有的那样。
发送一个小标题,它只包含一个float32最小值和最大值,然后你可以将信息作为16位插值在两者之间发送。正如您所说,精度不是问题,您甚至可以一次发送8位。
在重建时你的价值会是这样的:
float t = _t / numeric_limits<unsigned short>::max(); // With casting, naturally ;)
float val = h.min + t * (h.max - h.min);
希望有所帮助。
- 汤姆
答案 5 :(得分:3)
这个问题已经有点陈旧了,但为了完整起见,你可能还会看一下this paper的半浮动和浮动半转换。
他们使用无分支表驱动方法和相对较小的查找表。它完全符合IEEE标准,甚至在性能上打败了Phernost的符合IEEE标准的无分支转换例程(至少在我的机器上)。但是他的代码当然更适合SSE,并且不易受内存延迟影响。
答案 6 :(得分:3)
此处其他答案中描述的大多数方法要么在从浮点数转换为一半时没有正确舍入,要么丢弃次正规值这是一个问题,因为2 ** - 14成为您最小的非零数字,或做不幸的事情与Inf / NaN。 Inf也是一个问题,因为最大有限数量的一半有点小于2 ^ 16。 OpenEXR不必要地缓慢而复杂,最后我看了一下。快速正确的方法将使用FPU进行转换,既可以作为直接指令,也可以使用FPU舍入硬件来实现正确的转换。任何一半到浮点转换应该不比一个2 ^ 16元素查找表慢。
以下是很难被击败的:
在OS X / iOS上,您可以使用vImageConvert_PlanarFtoPlanar16F和vImageConvert_Planar16FtoPlanarF。请参阅Accelerate.framework。
英特尔ivybridge为此添加了SSE指令。见f16cintrin.h。 类似的指令被添加到ARM ISA for Neon中。请参阅arm_neon.h中的vcvt_f32_f16和vcvt_f16_f32。在iOS上,您需要使用arm64或armv7s arch来访问它们。
答案 7 :(得分:3)
此代码将32位浮点数转换为16位并返回。
#include <x86intrin.h>
#include <iostream>
int main()
{
float f32;
unsigned short f16;
f32 = 3.14159265358979323846;
f16 = _cvtss_sh(f32, 0);
std::cout << f32 << std::endl;
f32 = _cvtsh_ss(f16);
std::cout << f32 << std::endl;
return 0;
}
我使用英特尔icpc 16.0.2测试:
$ icpc a.cpp
g ++ 7.3.0:
$ g++ -march=native a.cpp
和clang ++ 6.0.0:
$ clang++ -march=native a.cpp
打印:
$ ./a.out
3.14159
3.14062
有关这些内在函数的文档可在以下网址获得:
答案 8 :(得分:2)
为什么这么复杂?我的实现不需要任何额外的库,符合IEEE-754 FP16格式,可以管理归一化和非归一化的数字,无分支,来回转换和沟渠NaN
大约需要40个时钟周期或Inf
扩展范围。这就是位运算的神奇力量。
typedef unsigned short ushort;
typedef unsigned int uint;
uint as_uint(const float x) {
return *(uint*)&x;
}
float as_float(const uint x) {
return *(float*)&x;
}
float half_to_float(const ushort x) { // IEEE-754 16-bit floating-point format (without infinity): 1-5-10, exp-15, +-131008.0, +-6.1035156E-5, +-5.9604645E-8, 3.311 digits
const uint e = (x&0x7C00)>>10; // exponent
const uint m = (x&0x03FF)<<13; // mantissa
const uint v = as_uint((float)m)>>23; // evil log2 bit hack to count leading zeros in denormalized format
return as_float((x&0x8000)<<16 | (e!=0)*((e+112)<<23|m) | ((e==0)&(m!=0))*((v-37)<<23|((m<<(150-v))&0x007FE000))); // sign : normalized : denormalized
}
ushort float_to_half(const float x) { // IEEE-754 16-bit floating-point format (without infinity): 1-5-10, exp-15, +-131008.0, +-6.1035156E-5, +-5.9604645E-8, 3.311 digits
const uint b = as_uint(x)+0x00001000; // round-to-nearest-even: add last bit after truncated mantissa
const uint e = (b&0x7F800000)>>23; // exponent
const uint m = b&0x007FFFFF; // mantissa; in line below: 0x007FF000 = 0x00800000-0x00001000 = decimal indicator flag - initial rounding
return (b&0x80000000)>>16 | (e>112)*((((e-112)<<10)&0x7C00)|m>>13) | ((e<113)&(e>101))*((((0x007FF000+m)>>(125-e))+1)>>1) | (e>143)*0x7FFF; // sign : normalized : denormalized : saturate
}
答案 9 :(得分:1)
对于不必考虑无穷大或NaN的情况,这种16到32位浮点的转换非常快,并且可以接受非正规零(DAZ)。即它适用于对性能敏感的计算,但如果您希望遇到非正规计算,则应注意除以零。
请注意,这最适合x86或其他具有条件移动或“设置为if”等效的平台。
反过来适用于单精度到半精度,但还有一些补充。
void float32(float* __restrict out, const uint16_t in) {
uint32_t t1;
uint32_t t2;
uint32_t t3;
t1 = in & 0x7fff; // Non-sign bits
t2 = in & 0x8000; // Sign bit
t3 = in & 0x7c00; // Exponent
t1 <<= 13; // Align mantissa on MSB
t2 <<= 16; // Shift sign bit into position
t1 += 0x38000000; // Adjust bias
t1 = (t3 == 0 ? 0 : t1); // Denormals-as-zero
t1 |= t2; // Re-insert sign bit
*((uint32_t*)out) = t1;
};
void float16(uint16_t* __restrict out, const float in) {
uint32_t inu = *((uint32_t*)&in);
uint32_t t1;
uint32_t t2;
uint32_t t3;
t1 = inu & 0x7fffffff; // Non-sign bits
t2 = inu & 0x80000000; // Sign bit
t3 = inu & 0x7f800000; // Exponent
t1 >>= 13; // Align mantissa on MSB
t2 >>= 16; // Shift sign bit into position
t1 -= 0x1c000; // Adjust bias
t1 = (t3 > 0x38800000) ? 0 : t1; // Flush-to-zero
t1 = (t3 < 0x8e000000) ? 0x7bff : t1; // Clamp-to-max
t1 = (t3 == 0 ? 0 : t1); // Denormals-as-zero
t1 |= t2; // Re-insert sign bit
*((uint16_t*)out) = t1;
};
请注意,您可以将常量0x7bff
更改为0x7c00
,以使其溢出至无穷大。
有关源代码,请参阅GitHub。
答案 10 :(得分:0)
这个问题很老,已经得到了解答,但我认为值得一提的是一个开源C ++库,它可以创建符合16位IEEE标准的半精度浮点数,并且有一个与内置浮点类型完全相同的类,但是有16位而不是32位。它是"half" class of the OpenEXR library。该代码采用宽松的BSD风格许可证。我不相信它在标准库之外有任何依赖。
答案 11 :(得分:0)
我遇到了同样的问题,发现this link非常有帮助。只需将文件“ieeehalfprecision.c”导入您的项目并使用它:
float myFloat = 1.24;
uint16_t resultInHalf;
singles2halfp(&resultInHalf, &myFloat, 1); // it accepts a series of floats, so use 1 to input 1 float
// an example to revert the half float back
float resultInSingle;
halfp2singles(&resultInSingle, &resultInHalf, 1);
我也更改了一些代码(请参阅作者的评论(James Tursa)):
#define INT16_TYPE int16_t
#define UINT16_TYPE uint16_t
#define INT32_TYPE int32_t
#define UINT32_TYPE uint32_t