Подтвердить что ты не робот

Преобразование с плавающей запятой 32-бит в 16 бит

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

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

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

4b9b3361

Ответ 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