32-разрядное в 16-разрядное преобразование с плавающей запятой - PullRequest
35 голосов
/ 02 ноября 2009

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

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

Отлично подойдет 16-битный формат IEEE.

Ответы [ 12 ]

48 голосов
/ 22 августа 2010

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

Так что это много, но оно обрабатывает все субнормальные значения, как бесконечности, тихие NaN, сигнальные NaN, так и отрицательный ноль. Конечно, полная поддержка 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, бесконечностей или отрицательного нуля. Эпсилон является наименьшим допустимым значением в диапазоне. Точность - это то, сколько битов точности нужно сохранить от поплавка. Хотя выше много ветвей, они все статичны и будут кэшироваться предиктором ветвления в CPU.

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

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

18 голосов
/ 02 ноября 2009

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

18 голосов
/ 02 ноября 2009

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

14 голосов
/ 06 ноября 2014

Половина, чтобы плавать:
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 голосов
/ 02 ноября 2009

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

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

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

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

Надеюсь, это поможет.

-Tom

3 голосов
/ 29 марта 2017

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

$ icpc a.cpp

г ++ 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

3 голосов
/ 25 марта 2015

Большинство подходов, описанных в других ответах здесь, либо неправильно округляются при преобразовании из числа с плавающей запятой, отбрасывают субнормалы, что является проблемой, поскольку 2 ** - 14 становится вашим наименьшим ненулевым числом, либо делают неудачные вещи с Inf / NaN. Inf также является проблемой, потому что наибольшее конечное число пополам немного меньше 2 ^ 16. OpenEXR был излишне медленным и сложным, в последний раз я смотрел на него. Быстрый корректный подход будет использовать FPU для выполнения преобразования, либо в качестве прямой инструкции, либо с использованием оборудования округления FPU, чтобы добиться нужной цели. Любое преобразование с половиной в плавающее не должно быть медленнее, чем таблица поиска 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, чтобы получить к ним доступ.

3 голосов
/ 23 февраля 2012

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

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

1 голос
/ 12 мая 2017

Я нашел реализацию преобразования из формата наполовину с плавающей точкой в ​​формат с плавающей точкой и обратно с использованием 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);
}
1 голос
/ 27 февраля 2013

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

Обратите внимание, что это больше всего подходит для x86 или других платформ, которые имеют условные перемещения или эквиваленты "set 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;
};

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

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

...