43

我需要一个跨平台的库/算法,它将在 32 位和 16 位浮点数之间进行转换。我不需要对 16 位数字进行数学运算;我只需要减小 32 位浮点数的大小,以便它们可以通过网络发送。我正在使用 C++。

我知道我会损失多少精度,但这对我的应用程序来说没问题。

IEEE 16 位格式会很棒。

4

14 回答 14

58

完成从单精度到半精度的转换。这是我的 SSE 版本的直接副本,因此它是无分支的。它利用了执行-true == ~0无分支选择的事实(GCC 将if语句转换为一团糟的条件跳转,而 Clang 只是将它们转换为条件移动。)

更新 (2019-11-04):重新设计以支持具有完全正确舍入的单精度双精度值。为了清楚起见,我还在每个无分支选择上方放置了相应的if语句作为注释。所有传入的 NaN 都将转换为基本的安静 NaN,以提高速度和完整性,因为无法在格式之间可靠地转换嵌入的 NaN 消息。

#include <cstdint> // uint32_t, uint64_t, etc.
#include <cstring> // memcpy
#include <climits> // CHAR_BIT
#include <limits>  // numeric_limits
#include <utility> // is_integral_v, is_floating_point_v, forward

namespace std
{
  template< typename T , typename U >
  T bit_cast( U&& u ) {
    static_assert( sizeof( T ) == sizeof( U ) );
    union { T t; }; // prevent construction
    std::memcpy( &t, &u, sizeof( t ) );
    return t;
  }
} // namespace std

template< typename T > struct native_float_bits;
template<> struct native_float_bits< float >{ using type = std::uint32_t; };
template<> struct native_float_bits< double >{ using type = std::uint64_t; };
template< typename T > using native_float_bits_t = typename native_float_bits< T >::type;

static_assert( sizeof( float ) == sizeof( native_float_bits_t< float > ) );
static_assert( sizeof( double ) == sizeof( native_float_bits_t< double > ) );

template< typename T, int SIG_BITS, int EXP_BITS >
struct raw_float_type_info {
  using raw_type = T;

  static constexpr int sig_bits = SIG_BITS;
  static constexpr int exp_bits = EXP_BITS;
  static constexpr int bits = sig_bits + exp_bits + 1;

  static_assert( std::is_integral_v< raw_type > );
  static_assert( sig_bits >= 0 );
  static_assert( exp_bits >= 0 );
  static_assert( bits <= sizeof( raw_type ) * CHAR_BIT );

  static constexpr int exp_max = ( 1 << exp_bits ) - 1;
  static constexpr int exp_bias = exp_max >> 1;

  static constexpr raw_type sign = raw_type( 1 ) << ( bits - 1 );
  static constexpr raw_type inf = raw_type( exp_max ) << sig_bits;
  static constexpr raw_type qnan = inf | ( inf >> 1 );

  static constexpr auto abs( raw_type v ) { return raw_type( v & ( sign - 1 ) ); }
  static constexpr bool is_nan( raw_type v ) { return abs( v ) > inf; }
  static constexpr bool is_inf( raw_type v ) { return abs( v ) == inf; }
  static constexpr bool is_zero( raw_type v ) { return abs( v ) == 0; }
};
using raw_flt16_type_info = raw_float_type_info< std::uint16_t, 10, 5 >;
using raw_flt32_type_info = raw_float_type_info< std::uint32_t, 23, 8 >;
using raw_flt64_type_info = raw_float_type_info< std::uint64_t, 52, 11 >;
//using raw_flt128_type_info = raw_float_type_info< uint128_t, 112, 15 >;

template< typename T, int SIG_BITS = std::numeric_limits< T >::digits - 1,
  int EXP_BITS = sizeof( T ) * CHAR_BIT - SIG_BITS - 1 >
struct float_type_info 
: raw_float_type_info< native_float_bits_t< T >, SIG_BITS, EXP_BITS > {
  using flt_type = T;
  static_assert( std::is_floating_point_v< flt_type > );
};

template< typename E >
struct raw_float_encoder
{
  using enc = E;
  using enc_type = typename enc::raw_type;

  template< bool DO_ROUNDING, typename F >
  static auto encode( F value )
  {
    using flt = float_type_info< F >;
    using raw_type = typename flt::raw_type;
    static constexpr auto sig_diff = flt::sig_bits - enc::sig_bits;
    static constexpr auto bit_diff = flt::bits - enc::bits;
    static constexpr auto do_rounding = DO_ROUNDING && sig_diff > 0;
    static constexpr auto bias_mul = raw_type( enc::exp_bias ) << flt::sig_bits;
    if constexpr( !do_rounding ) { // fix exp bias
      // when not rounding, fix exp first to avoid mixing float and binary ops
      value *= std::bit_cast< F >( bias_mul );
    }
    auto bits = std::bit_cast< raw_type >( value );
    auto sign = bits & flt::sign; // save sign
    bits ^= sign; // clear sign
    auto is_nan = flt::inf < bits; // compare before rounding!!
    if constexpr( do_rounding ) {
      static constexpr auto min_norm = raw_type( flt::exp_bias - enc::exp_bias + 1 ) << flt::sig_bits;
      static constexpr auto sub_rnd = enc::exp_bias < sig_diff
        ? raw_type( 1 ) << ( flt::sig_bits - 1 + enc::exp_bias - sig_diff )
        : raw_type( enc::exp_bias - sig_diff ) << flt::sig_bits;
      static constexpr auto sub_mul = raw_type( flt::exp_bias + sig_diff ) << flt::sig_bits;
      bool is_sub = bits < min_norm;
      auto norm = std::bit_cast< F >( bits );
      auto subn = norm;
      subn *= std::bit_cast< F >( sub_rnd ); // round subnormals
      subn *= std::bit_cast< F >( sub_mul ); // correct subnormal exp
      norm *= std::bit_cast< F >( bias_mul ); // fix exp bias
      bits = std::bit_cast< raw_type >( norm );
      bits += ( bits >> sig_diff ) & 1; // add tie breaking bias
      bits += ( raw_type( 1 ) << ( sig_diff - 1 ) ) - 1; // round up to half
      //if( is_sub ) bits = std::bit_cast< raw_type >( subn );
      bits ^= -is_sub & ( std::bit_cast< raw_type >( subn ) ^ bits );
    }
    bits >>= sig_diff; // truncate
    //if( enc::inf < bits ) bits = enc::inf; // fix overflow
    bits ^= -( enc::inf < bits ) & ( enc::inf ^ bits );
    //if( is_nan ) bits = enc::qnan;
    bits ^= -is_nan & ( enc::qnan ^ bits );
    bits |= sign >> bit_diff; // restore sign
    return enc_type( bits );
  }

  template< typename F >
  static F decode( enc_type value )
  {
    using flt = float_type_info< F >;
    using raw_type = typename flt::raw_type;
    static constexpr auto sig_diff = flt::sig_bits - enc::sig_bits;
    static constexpr auto bit_diff = flt::bits - enc::bits;
    static constexpr auto bias_mul = raw_type( 2 * flt::exp_bias - enc::exp_bias ) << flt::sig_bits;
    raw_type bits = value;
    auto sign = bits & enc::sign; // save sign
    bits ^= sign; // clear sign
    auto is_norm = bits < enc::inf;
    bits = ( sign << bit_diff ) | ( bits << sig_diff );
    auto val = std::bit_cast< F >( bits ) * std::bit_cast< F >( bias_mul );
    bits = std::bit_cast< raw_type >( val );
    //if( !is_norm ) bits |= flt::inf;
    bits |= -!is_norm & flt::inf;
    return std::bit_cast< F >( bits );
  }
};

using flt16_encoder = raw_float_encoder< raw_flt16_type_info >;

template< typename F >
auto quick_encode_flt16( F && value )
{ return flt16_encoder::encode< false >( std::forward< F >( value ) ); }

template< typename F >
auto encode_flt16( F && value )
{ return flt16_encoder::encode< true >( std::forward< F >( value ) ); }

template< typename F = float, typename X >
auto decode_flt16( X && value )
{ return flt16_encoder::decode< F >( std::forward< X >( value ) ); }

当然,并不总是需要完整的 IEEE 支持。如果您的值不需要接近零的对数分辨率,那么如前所述,将它们线性化为定点格式会快得多。

于 2010-08-22T19:23:38.860 回答
22

一半浮动:
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);

于 2014-11-06T12:09:06.480 回答
20

std::frexp从普通浮点数或双精度数中提取有效数和指数 - 然后您需要决定如何处理太大而无法放入半精度浮点数(饱和...?)的指数,进行相应调整,然后将一半-精度数在一起。 本文有 C 源代码向您展示如何执行转换。

于 2009-11-02T04:55:25.483 回答
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,您将获得更高的准确性,但某些整数将在另一端以小数形式结束。

于 2009-11-02T05:35:29.210 回答
10

为什么这么复杂?我的实现不需要任何额外的库,符合 IEEE-754 FP16 格式,管理规范化和非规范化数字,无分支,来回转换和沟渠NaNInf扩展范围需要大约 40 个时钟周期. 这就是位运算的神奇力量。

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
}

如何使用它并检查转换是否正确的示例:

#include <iostream>

void print_bits(const ushort x) {
    for(int i=15; i>=0; i--) {
        cout << ((x>>i)&1);
        if(i==15||i==10) cout << " ";
        if(i==10) cout << "      ";
    }
    cout << endl;
}
void print_bits(const float x) {
    uint b = *(uint*)&x;
    for(int i=31; i>=0; i--) {
        cout << ((b>>i)&1);
        if(i==31||i==23) cout << " ";
        if(i==23) cout << "   ";
    }
    cout << endl;
}

int main() {
    const float x = 1.0f;
    const ushort x_compressed = float_to_half(x);
    const float x_decompressed = half_to_float(x_compressed);
    print_bits(x);
    print_bits(x_compressed);
    print_bits(x_decompressed);
    return 0;
}

输出:

0 01111111    00000000000000000000000
0 01111       0000000000
0 01111111    00000000000000000000000
于 2020-02-03T21:19:30.253 回答
5

如果您要发送信息流,您可能会做得比这更好,特别是如果一切都在一致的范围内,就像您的应用程序似乎有的那样。

发送一个小标题,它只包含一个 float32 最小值和最大值,然后您可以将您的信息作为两者之间的 16 位插值值发送。正如您还说精度不是什么大问题,您甚至可以一次发送 8 位。

在重建时,您的价值将类似于:

float t = _t / numeric_limits<unsigned short>::max();  // With casting, naturally ;)
float val = h.min + t * (h.max - h.min);

希望有帮助。

-汤姆

于 2009-11-02T10:01:32.237 回答
4

这个问题已经有点老了,但为了完整起见,你也可以看看这篇论文进行半浮点和半浮点转换。

他们使用具有相对较小查找表的无分支表驱动方法。它完全符合 IEEE 标准,甚至在性能上击败了 Phernost 的符合 IEEE 标准的无分支转换例程(至少在我的机器上)。但当然,他的代码更适合 SSE,并且不太容易受到内存延迟效应的影响。

于 2012-02-23T17:08:53.140 回答
4

此处其他答案中描述的大多数方法要么在从浮点数转换为一半时无法正确舍入,要么丢弃次正规数,这是一个问题,因为 2**-14 成为您最小的非零数,或者使用 Inf / 做不幸的事情南。Inf 也是一个问题,因为一半的最大有限数略小于 2^16。OpenEXR 是不必要的缓慢和复杂,最后我看了一下。一种快速正确的方法将使用 FPU 进行转换,或者作为直接指令,或者使用 FPU 舍入硬件来使正确的事情发生。任何半浮点转换都不应比 2^16 元素查找表慢。

以下是难以击败的:

在 OS X / iOS 上,您可以使用 vImageConvert_PlanarFtoPlanar16F 和 vImageConvert_Planar16FtoPlanarF。请参阅 Accelerate.framework。

英特尔 ivybridge 为此添加了 SSE 指令。参见 f16cintrin.h。Neon 的 ARM ISA 中添加了类似的指令。请参见 arm_neon.h 中的 vcvt_f32_f16 和 vcvt_f16_f32。在 iOS 上,您需要使用 arm64 或 armv7s arch 来访问它们。

于 2015-03-25T18:21:37.357 回答
4

对于不必考虑无穷大或 NaN 并且可以接受非正规为零 (DAZ) 的情况,这种 16 到 32 位浮点的转换非常快。即它适用于对性能敏感的计算,但如果您预计会遇到非规范化,则应注意除以零。

请注意,这最适合 x86 或其他具有条件移动或“设置 if”等价物的平台。

  1. 从输入中去除符号位
  2. 将尾数的最高有效位与第 22 位对齐
  3. 调整指数偏差
  4. 如果输入指数为零,则将位设置为零
  5. 重新插入符号位

相反适用于单精度到半精度,并添加了一些内容。

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;
};

请注意,您可以将常量更改0x7bff0x7c00使其溢出到无穷大。

有关源代码,请参见GitHub

于 2013-02-27T17:18:58.543 回答
4

此代码将 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

和铿锵++ 6.0.0:

$ clang++ -march=native a.cpp

它打印:

$ ./a.out
3.14159
3.14062

有关这些内在函数的文档可在以下位置获得:

https://software.intel.com/en-us/node/524287

https://clang.llvm.org/doxygen/f16cintrin_8h.html

于 2017-03-28T23:32:17.070 回答
1

我遇到了同样的问题,发现这个链接非常有帮助。只需将文件“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
于 2014-09-09T04:50:05.430 回答
1

这个问题很老,已经得到解答,但我认为值得一提的是一个开源 C++ 库,它可以创建 16 位 IEEE 兼容的半精度浮点数,并且有一个与内置浮点类型几乎相同的类,但具有16 位而不是 32 位。它是OpenEXR 库的“半”类。该代码在一个宽松的 BSD 风格许可证下。我不相信它在标准库之外有任何依赖。

于 2012-12-12T01:52:35.297 回答
1

我发现了一个使用 AVX2 从半浮点格式转换为单浮点格式并返回的实现这些算法比软件实现要快得多。我希望它会有用。

32 位浮点数到 16 位浮点数的转换:

#include <immintrin.h"

inline void Float32ToFloat16(const float * src, uint16_t * dst)
{
    _mm_storeu_si128((__m128i*)dst, _mm256_cvtps_ph(_mm256_loadu_ps(src), 0));
}

void Float32ToFloat16(const float * src, size_t size, uint16_t * dst)
{
    assert(size >= 8);

    size_t fullAlignedSize = size&~(32-1);
    size_t partialAlignedSize = size&~(8-1);

    size_t i = 0;
    for (; i < fullAlignedSize; i += 32)
    {
        Float32ToFloat16(src + i + 0, dst + i + 0);
        Float32ToFloat16(src + i + 8, dst + i + 8);
        Float32ToFloat16(src + i + 16, dst + i + 16);
        Float32ToFloat16(src + i + 24, dst + i + 24);
    }
    for (; i < partialAlignedSize; i += 8)
        Float32ToFloat16(src + i, dst + i);
    if(partialAlignedSize != size)
        Float32ToFloat16(src + size - 8, dst + size - 8);
}

16 位浮点数到 32 位浮点数的转换:

#include <immintrin.h"

inline void Float16ToFloat32(const uint16_t * src, float * dst)
{
    _mm256_storeu_ps(dst, _mm256_cvtph_ps(_mm_loadu_si128((__m128i*)src)));
}

void Float16ToFloat32(const uint16_t * src, size_t size, float * dst)
{
    assert(size >= 8);

    size_t fullAlignedSize = size&~(32-1);
    size_t partialAlignedSize = size&~(8-1);

    size_t i = 0;
    for (; i < fullAlignedSize; i += 32)
    {
        Float16ToFloat32<align>(src + i + 0, dst + i + 0);
        Float16ToFloat32<align>(src + i + 8, dst + i + 8);
        Float16ToFloat32<align>(src + i + 16, dst + i + 16);
        Float16ToFloat32<align>(src + i + 24, dst + i + 24);
    }
    for (; i < partialAlignedSize; i += 8)
        Float16ToFloat32<align>(src + i, dst + i);
    if (partialAlignedSize != size)
        Float16ToFloat32<false>(src + size - 8, dst + size - 8);
}
于 2017-05-12T10:03:48.727 回答
0

感谢小数到单精度的代码

我们实际上可以尝试将相同的代码编辑到半精度,但是使用 gcc C 编译器是不可能的,所以请执行以下操作

sudo apt install clang

然后试试下面的代码

// A C code to convert Decimal value to IEEE 16-bit floating point Half precision

#include <stdio.h>

void printBinary(int n, int i)
{
 

    int k;
    for (k = i - 1; k >= 0; k--) {
 
        if ((n >> k) & 1)
            printf("1");
        else
            printf("0");
    }
}
 
typedef union {
    
    __fp16 f;
    struct
    {
        unsigned int mantissa : 10;
        unsigned int exponent : 5;
        unsigned int sign : 1;
 
    } raw;
} myfloat;
 

// Driver Code
int main()
{
    myfloat var;
    var.f = 11;
    printf("%d | ", var.raw.sign);
    printBinary(var.raw.exponent, 5);
    printf(" | ");
    printBinary(var.raw.mantissa, 10);
    printf("\n");
    return 0;
}

在终端中编译代码

clang code_name.c -o code_name
./code_name

这里

__fp16

clang C 编译器支持的 2 字节浮点数据类型

于 2022-02-17T17:50:11.700 回答