Преобразование с плавающей запятой 32-бит в 16 бит
Мне нужна кросс-платформенная библиотека/алгоритм, который будет конвертировать между 32-битными и 16-разрядными числами с плавающей запятой. Мне не нужно выполнять математику с 16-разрядными номерами; Мне просто нужно уменьшить размер 32-битных поплавков, чтобы они могли быть отправлены по сети. Я работаю на С++.
Я понимаю, насколько сильно я бы проиграл, но это нормально для моего приложения.
16-разрядный формат IEEE будет отличным.
Ответы
Ответ 1
std::frexp
извлекает значение и экспоненту из обычных поплавков или удваивает - тогда вам нужно решить, что делать с показателями, которые тоже большой, чтобы поместиться в полуточную поплавку (насыщенный...?), отрегулировать соответственно и поместить число полуточности вместе. Эта статья содержит исходный код C, чтобы показать вам, как выполнить преобразование.
Ответ 2
Полное преобразование из одной точности в половину точности. Это прямая копия из моей версии 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;
}
};
Таким образом, чтобы многое принять, но он обрабатывает все субнормальные значения, как бесконечности, так и спокойные NaNs, сигнализирующие NaNs и отрицательные ноль. Конечно, полная поддержка 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) в графической библиотеке для уменьшения размера линейных значений цвета float в памяти. Сжатые поплавки имеют преимущество в создании небольших поисковых таблиц для длительных функций, таких как гамма-коррекция или трансцендентные. Сжатие линейных значений sRGB уменьшается до максимального 12 бит или максимального значения 3011, что отлично подходит для размера таблицы поиска для/из sRGB.
Ответ 3
Учитывая ваши потребности (-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, вы получите немного больше точности, но некоторые целые числа будут заканчиваться как десятичные числа на другом конце.
Ответ 4
Половина для плавания:
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);
Ответ 5
Если вы отправляете поток информации по всему, вы, вероятно, можете сделать лучше, чем это, особенно если все находится в согласованном диапазоне, как кажется вашему приложению.
Отправьте небольшой заголовок, который состоит только из минимума и максимума float32, затем вы можете отправить свою информацию в виде 16-разрядного значения интерполяции между ними. Поскольку вы также говорите, что точность не большая проблема, вы даже можете отправить 8 бит за раз.
Ваше значение будет во время восстановления:
float t = _t / numeric_limits<unsigned short>::max(); // With casting, naturally ;)
float val = h.min + t * (h.max - h.min);
Надеюсь, что это поможет.
-Tom
Ответ 6
Этот вопрос уже немного устарел, но, ради полноты, вы можете также взглянуть на этот документ для половины -float и float-to-half.
Они используют подход, основанный на разветвленной табличке, с относительно небольшими справочными таблицами. Он полностью совместим с IEEE и даже превосходит Panchost IEEE-совместимые процедуры безрезультатного преобразования в производительности (по крайней мере, на моей машине). Но, конечно, его код намного лучше подходит для SSE и не подвержен эффектам латентности памяти.
Ответ 7
Большинство подходов, описанных в других ответах, здесь либо не округляют правильно при преобразовании от float до половины, выбрасывают субнормали, что является проблемой, так как 2 ** - 14 становится вашим наименьшим ненулевым числом или делает неудачные вещи с Inf/NaN. Inf также является проблемой, потому что наибольшее конечное число пополам немного меньше 2 ^ 16. OpenEXR был излишне медленным и сложным, в последний раз я смотрел на него. Быстрый верный подход будет использовать FPU для преобразования, либо в виде прямой инструкции, либо с помощью аппаратного обеспечения округления FPU, чтобы сделать правильную вещь. Любое преобразование с половиной в float должно быть не медленнее, чем таблица поиска элементов 2 ^ 16.
Трудно побить следующее:
В OS X/iOS вы можете использовать vImageConvert_PlanarFtoPlanar16F и vImageConvert_Planar16FtoPlanarF. См. Раздел Accelerate.framework.
Intel ivybridge добавил для этого инструкции SSE. См. F16cintrin.h.
Аналогичные инструкции были добавлены в ARM ISA для Neon. См. Vcvt_f32_f16 и vcvt_f16_f32 в arm_neon.h. На iOS вам нужно будет использовать арку arm64 или armv7s для доступа к ним.
Ответ 8
Этот код преобразует 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;
}
Я тестировал с помощью компилятора Intel icpc версии 16.0.2. Он печатает:
3.14159
3.14062
Документация об этих функциях доступна по адресу:
https://software.intel.com/en-us/node/524287
https://clang.llvm.org/doxygen/f16cintrin_8h.html
Ответ 9
Это преобразование для плавающей точки с 16 по 32 бита довольно быстро для случаев, когда вам не нужно учитывать бесконечность или NaN, и может принимать denormals-as-zero (DAZ). То есть он подходит для высокочувствительных вычислений, но вы должны остерегаться деления на ноль, если вы ожидаете столкнуться с денормалами.
Обратите внимание, что это наиболее подходит для x86 или других платформ, которые имеют условные ходы или эквиваленты "set if".
- Сбросьте бит знака с входа
- Совместите самый значительный бит мантиссы с 22-м битом
- Отрегулируйте смещение экспоненты
- Установите биты на все ноль, если индекс ввода равен нулю
- Вставить знаковый бит
Обратное применяется для однократной точности с некоторыми дополнениями.
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
Я нашел реализацию преобразования из полу-float в формат с одним поплавком и обратно с использованием 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);
}
Ответ 11
Вопрос старен и уже был дан ответ, но я решил, что стоит упомянуть библиотеку С++ с открытым исходным кодом, которая может создавать 16-битные IEEE-совместимые полуточности с плавающей запятой и имеет класс, который действует практически идентично встроенному поплавковому типу, но с 16 бит вместо 32. Это "half" класс библиотеки OpenEXR. Код находится под разрешительной лицензией BSD. Я не считаю, что он имеет какие-либо зависимости вне стандартной библиотеки.
Ответ 12
У меня была такая же самая точная проблема, и я нашел эту ссылку очень полезной. Просто импортируйте файл "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);
Я также меняю код (см. комментарий автора (Джеймс Турса) в ссылке):
#define INT16_TYPE int16_t
#define UINT16_TYPE uint16_t
#define INT32_TYPE int32_t
#define UINT32_TYPE uint32_t