2009-11-02 2 views
30

Мне нужна кросс-платформенная библиотека/алгоритм, который будет конвертировать между 32-битными и 16-разрядными числами с плавающей запятой. Мне не нужно выполнять математику с 16-разрядными номерами; Мне просто нужно уменьшить размер 32-битных поплавков, чтобы они могли быть отправлены по сети. Я работаю на C++.Преобразование с плавающей запятой 32-бит в 16 бит

Я понимаю, насколько точно я буду проигрывать, но это нормально для моего приложения.

16-разрядный формат IEEE был бы замечательным.

+0

Вы уверены, что вы будете в состоянии измерить эффективность выгоды от этого преобразования? Вам нужно будет отправлять много этих номеров по проводам, чтобы добиться значительной экономии. Вы получаете только 3 десятичные цифры точности, и диапазон не так уж и большой. –

+0

OTOH, CPU по существу свободен в настоящее время, если вы можете потопить свою программу, а преобразование потока ввода-вывода легко нарезается резьбой.Экономия при вводе-выводе будет реальной, если количество отправленных посылок находится где угодно рядом с пропускной способностью сети. То есть это хороший компромисс между пропускной способностью и временем ожидания и, как таковой, имеет значение только тогда, когда у вас действительно есть проблема с bandwitdh и проблемы с задержкой. – MSalters

+0

Есть ли у C++ встроенная поддержка 16-битных поплавков? – Lazer

ответ

17

std::frexp извлекает значение и экспоненту из обычных поплавков или парных разрядов - тогда вам нужно решить, что делать с показателями, которые слишком велики, чтобы поместиться в полуточную поплавку (насыщенный ...?), и соедините половину точности. This article имеет исходный код C, чтобы показать вам, как выполнить преобразование.

+0

Фактически, значения, которые я отправляю, имеют очень ограниченный диапазон: (-1000, 1000), поэтому экспонент не так уж и большой. –

+0

@Matt, если вы ** знаете ** экспонента никогда не будет под/над потоком, тогда ваша работа будет проще в этом! -) –

+0

@Alex, действительно, это облегчает! Благодарю. –

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, вы получите немного больше точности, но некоторые целые числа будут заканчиваться как десятичные знаки на другом конце.

+0

+1 Это даст вам гораздо больше точности, чем 16-битный поплавок почти в каждом случае и с меньшей математикой и без особых случаев. 16-битный IEEE-поплавок будет иметь только 10 бит точности и набирает половину своих возможных значений в диапазоне (-1, 1) –

+10

Это зависит от распределения в диапазоне [-1000, 1000]. Если большинство чисел на самом деле находится в диапазоне [-1,1], то точность 16 бит-поплавков в среднем лучше. – MSalters

+0

Это было бы лучше с SHORT_MAX и 1024 в качестве масштабного коэффициента, давая представление с фиксированной точкой 10.6 бит и все целые числа были бы точно представлены. Точность будет равна 1/2^6 = 0,015625, что намного лучше, чем 0,05, а коэффициент масштабирования мощности-два легко оптимизировать до бит-сдвига (компилятор, скорее всего, сделает это за вас). – Clifford

4

Если вы отправляете поток информации по всему, вы, вероятно, можете сделать это лучше, особенно если все в согласованном диапазоне, как кажется вашему приложению.

Отправьте небольшой заголовок, который состоит только из минимума и максимума float32, затем вы можете отправить свою информацию в виде 16-битного значения интерполяции между ними. Поскольку вы также говорите, что точность не большая проблема, вы даже можете отправить 8 бит за раз.

Ваше значение будет что-то подобное, во время реконструкции:

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

Надежда, что помогает.

-Tom

+0

Это отличное решение, особенно для нормализованных значений вектора/кватерниона, которые, как вы знаете, всегда будут находиться в диапазоне (-1, 1). –

+0

+1 для использования 'numeric_limits'. – xtofl

+0

проблема с использованием интерполяции вместо простого масштабирования состоит в том, что нуль точно не представлен, а некоторые системы чувствительны к такому, как матрица матрицы 4x4. например, (min, max-min) есть (-11.356439590454102, 23.32344913482666), то ближайшим вы можете добраться до нуля 0,00010671140473306195. – milkplus

44

Полное преобразование с одной точностью в половину точности. Это прямая копия из моей версии 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, сигнализацию и пренебрежимы малой отрицательный нуль. Конечно, полная поддержка 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; 
     } 

    }; 

Это заставляет все значения в допустимом диапазон, нет поддержки NaNs, бесконечности или отрицательного нуля. Epsilon является наименьшим допустимым значением в диапазоне. Прецизионность - это то, сколько бит точности следует сохранить из поплавка. Несмотря на то, что много ветвей выше, они все статичны и будут кэшироваться предиктором ветки в CPU.

Конечно, если ваши значения не требуют логарифмического разрешения, приближающегося к нулю. Тогда линеаризация их в формат фиксированной точки происходит намного быстрее, как уже упоминалось.

Я использую FloatCompressor (версия SSE) в графической библиотеке, чтобы уменьшить размер линейных значений цвета float в памяти.Сжатые поплавки имеют преимущество в создании небольших поисковых таблиц для длительных функций, таких как гамма-коррекция или трансцендентные. Сжатие линейных значений sRGB уменьшается до максимального 12 бит или максимального значения 3011, что отлично подходит для размера таблицы поиска для/из sRGB.

+0

В начале вы пишете, что он полагается на '(-true == ~ 0) GCC. Я хочу использовать фрагмент кода в Visual Studio 2012, есть ли пара входных данных + ожидаемая выходная пара, которые могли бы рассказать мне, правильно ли работает мой компилятор? Кажется, он обращается вперед и назад без проблем, и вышеупомянутое выражение остается верным. – Cygon

+2

Какова лицензия вашего класса Float16Compressor? –

+5

Unlicense (http://choosealicense.com/licenses/unlicense/), который является общедоступным. – Phernost

3

Этот вопрос уже немного устарел, но для полноты вы можете также взглянуть на this paper для преобразования с половиной поплавком и с плавающей точкой.

Они используют подход, основанный на разветвленной табличке, с относительно небольшими справочными таблицами. Он полностью совместим с IEEE и даже превосходит Phernost IEEE-совместимые бездисковые процедуры преобразования в производительности (по крайней мере, на моей машине). Но, конечно, его код намного лучше подходит для SSE и не подвержен эффектам латентности памяти.

+0

+1 Эта бумага очень хорошая. Обратите внимание, что он не является полностью * IEEE-совместимым в том, как он обрабатывает NaN. IEEE говорит, что число является NaN, только если установлен хотя бы один из битов мантиссы. Поскольку предоставленный код игнорирует биты младшего разряда, некоторые 32-битные NaN ошибочно преобразуются в Inf. Хотя вряд ли случится. –

0

Вопрос старый, на который уже был дан ответ, но я подумал, что стоит упомянуть библиотеку с открытым исходным кодом на C++, которая может создавать 16-битные IEEE-совместимые полуточности с плавающей запятой и имеет класс, который практически идентичен встроенному поплату тип, но с 16 битами вместо 32. Это "half" class of the OpenEXR library. Код находится под разрешительной лицензией BSD. Я не считаю, что он имеет какие-либо зависимости вне стандартной библиотеки.

+1

Хотя мы говорим о библиотеках с открытым исходным кодом C++, предоставляющих IEEE-совместимые типы полуточности, которые как можно больше действуют как встроенные типы с плавающей запятой, посмотрите на библиотеку [* half * library] (http: // half. sourceforge.net/) (отказ от ответственности: это от меня). –

1

Это преобразование для плавающей точки с 16 по 32 бит довольно быстро для случаев, когда вам не нужно учитывать бесконечность или NaN, и может принимать денормалы-ноль (DAZ). То есть он подходит для высокопроизводительных вычислений, но вы должны остерегаться деления на ноль, если вы ожидаете столкнуться с денормалами.

Обратите внимание, что это наиболее подходит для x86 или других платформ, имеющих условные ходы или эквиваленты «установить, если».

  1. Strip знаковый бит от входа
  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; 
}; 

Обратите внимание, что вы можете изменить константу 0x7bff к 0x7c00 для того, чтобы переполнить до бесконечности.

См. GitHub для исходного кода.

+0

Вы, вероятно, имели в виду '0x80000000' вместо' 0x7FFFFFFF', так как иначе вы делали бы обрезку abs. Последняя операция также может быть записана как: 't1 & = 0x80000000 | (static_cast (t3 == 0) -1) '. Хотя это, вероятно, зависит от платформы (ее чувствительность к отказам ветвления-предсказания, наличие инструкции условного присваивания ...) и компилятор (его способность генерировать соответствующий код для самой платформы), который лучше. Ваша версия может выглядеть лучше и понятнее тем, кто не знаком с бинарными операциями и правилами типа * C++ *. –

+0

Спасибо за то, что я включил ваши комментарии в ответ. –

+1

В float16 тест Clamp-to-max явно неправильный, он всегда срабатывает. Тест с флеш-к-ном имеет неправильный способ сравнения. Я считаю, что два теста должны быть: 't1 = (t3 <0x38800000)? 0: t1; 'и' t1 = (t3> 0x47000000)? 0x7bff: t1; ' – Frepa

0

У меня была такая же точная проблема, и нашлось this link очень полезно. Просто импортируйте файл "ieeehalfprecision.с»в свой проект и использовать его как это:

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 
10

Половина плавать:
float f = ((h&0x8000)<<16) | (((h&0x7c00)+0x1C000)<<13) | ((h&0x03FF)<<13);

Float до половины:
uint32_t x = *((uint32_t*)&f);
uint16_t h = ((x>>16)&0x8000)|((((x&0x7f800000)-0x38000000)>>13)&0x7c00)|((x>>13)&0x03ff);

+3

Но, конечно, имейте в виду, что в настоящее время он игнорирует любые переполнения, недотоки, денормализованные значения или бесконечные значения. –

3

Большинство подходов, описанных в других ответах, здесь либо не округляют правильно при преобразовании с поплавка на половину, выбрасывают субнормали, что является проблемой, поскольку 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 для доступа к ним.

2

Этот код преобразует 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

1

Я нашел implementation конверсий от половинного поплавка в формат одного поплавка и обратно с использованием 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); 
} 
Смежные вопросы