32ビットと16ビットの浮動小数点数の間で変換するクロスプラットフォームのライブラリ/アルゴリズムが必要です。16ビットの数値で数学を実行する必要はありません。32ビットフロートのサイズを小さくして、ネットワーク経由で送信できるようにする必要があります。私はC++で作業しています。
どれだけの精度が失われるかは理解していますが、それは私のアプリケーションでは問題ありません。
IEEE16ビット形式は素晴らしいでしょう。
32ビットと16ビットの浮動小数点数の間で変換するクロスプラットフォームのライブラリ/アルゴリズムが必要です。16ビットの数値で数学を実行する必要はありません。32ビットフロートのサイズを小さくして、ネットワーク経由で送信できるようにする必要があります。私はC++で作業しています。
どれだけの精度が失われるかは理解していますが、それは私のアプリケーションでは問題ありません。
IEEE16ビット形式は素晴らしいでしょう。
単精度から半精度への完全な変換。これは私のSSEバージョンからの直接コピーなので、ブランチはありません。これは、ブランチレス選択を実行するという事実を利用しています-true == ~0
(GCCif
はステートメントを条件付きジャンプの不潔な混乱に変換しますが、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サポートは必ずしも必要ではありません。値がゼロに近づく対数分解能を必要としない場合は、すでに述べたように、値を固定小数点形式に線形化する方がはるかに高速です。
半精度浮動小数点数:半精度浮動小数点数
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);
std::frexp
通常の浮動小数点数または倍精度浮動小数点数から仮数と指数を抽出します。次に、半精度浮動小数点数に収まらないほど大きい指数(飽和...?)をどうするかを決定し、それに応じて調整し、半分を置く必要があります-一緒に正確な数。 この記事には、変換の実行方法を示すCソースコードが含まれています。
ニーズ(-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に変更すると、精度は少し向上しますが、一部の整数は、もう一方の端で小数になります。
なぜそんなに複雑なのですか?私の実装は追加のライブラリを必要とせず、IEEE-754 FP16形式に準拠し、正規化数と非正規化数の両方を管理し、ブランチがなく、前後の変換と溝NaN
、またはInf
拡張範囲に約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
情報のストリームを送信している場合、特にアプリケーションが持っているようにすべてが一貫した範囲内にある場合は、おそらくこれよりもうまくいく可能性があります。
float32の最小値と最大値だけで構成される小さなヘッダーを送信すると、2つの間の16ビット補間値として情報を送信できます。精度はそれほど問題ではないとおっしゃっていますが、一度に8ビットを送信することもできます。
再構築時の値は次のようになります。
float t = _t / numeric_limits<unsigned short>::max(); // With casting, naturally ;)
float val = h.min + t * (h.max - h.min);
お役に立てば幸いです。
-トム
この質問はすでに少し古いですが、完全を期すために、半精度から浮動小数点への変換と半精度への変換についてこのペーパーを参照することもできます。
それらは、比較的小さなルックアップテーブルでブランチレステーブル駆動型アプローチを使用します。これは完全にIEEE準拠であり、PhernostのIEEE準拠のブランチレス変換ルーチンよりもパフォーマンスが優れています(少なくとも私のマシンでは)。しかしもちろん、彼のコードはSSEにはるかに適していて、メモリ遅延の影響を受けにくいです。
この16ビットから32ビットの浮動小数点への変換は、無限大やNaNを考慮する必要がなく、非正規化数(DAZ)を受け入れることができる場合に非常に高速です。つまり、パフォーマンスに敏感な計算には適していますが、非正規化数が発生することが予想される場合は、ゼロによる除算に注意する必要があります。
これは、条件付き移動または同等の「setif」があるx86またはその他のプラットフォームに最も適していることに注意してください。
逆は、いくつかの追加を加えて、単精度から半精度に適用されます。
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を参照してください。
ここで他の回答で説明されているアプローチのほとんどは、浮動小数点から半精度への変換時に正しく丸められないか、2 **-14が最小の非ゼロ数になるため問題となる非正規化数を破棄するか、Inf/で不幸なことをします。 NaN。半分の最大の有限数は2^16より少し小さいため、Infも問題になります。OpenEXRは不必要に遅くて複雑でした、最後に私はそれを見ました。高速で正しいアプローチでは、FPUを使用して、直接命令として変換を実行するか、FPU丸めハードウェアを使用して正しい処理を実行します。半精度から浮動小数点への変換は、2^16要素のルックアップテーブルより遅くないはずです。
以下を打ち負かすのは難しいです。
OS X / iOSでは、vImageConvert_PlanarFtoPlanar16FおよびvImageConvert_Planar16FtoPlanarFを使用できます。Accelerate.frameworkを参照してください。
Intel ivybridgeは、このためのSSE命令を追加しました。f16cintrin.hを参照してください。同様の手順がネオン用のARMISAに追加されました。arm_neon.hのvcvt_f32_f16およびvcvt_f16_f32を参照してください。iOSでは、arm64またはarmv7sarchを使用してそれらにアクセスする必要があります。
このコードは、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;
}
Intelicpc16.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
これらの組み込み関数に関するドキュメントは、次のURLで入手できます。
質問は古く、すでに回答されていますが、16ビットIEEE準拠の半精度浮動小数点数を作成でき、組み込みの浮動小数点型とほぼ同じように動作するクラスを持つオープンソースのC++ライブラリについて言及する価値があると思いました。 32ビットではなく16ビット。これはOpenEXRライブラリの「half」クラスです。このコードは、パーミッシブBSDスタイルのライセンスに基づいています。標準ライブラリ以外の依存関係はないと思います。
私はこれとまったく同じ問題を抱えていて、このリンクが非常に役立つことがわかりました。ファイル「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
ハーフフロート形式からシングルフロート形式への変換の実装を見つけ、 AVX2を使用して元に戻しました。これらのアルゴリズムのソフトウェア実装よりもはるかに高速です。お役に立てば幸いです。
32ビットfloatから16ビットfloatへの変換:
#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ビットfloatから32ビットfloatへの変換:
#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);
}
実際には、同じコードを半精度で編集することはできますが、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
clangCコンパイラでサポートされている2バイトのfloatデータ型です