web-dev-qa-db-fra.com

Conversion de virgule flottante 32 bits à 16 bits

J'ai besoin d'une bibliothèque/d'un algorithme multiplateforme qui permette la conversion entre nombres à virgule flottante 32 bits et 16 bits. Je n'ai pas besoin de faire des calculs avec les nombres à 16 bits; Je dois simplement réduire la taille des flottants 32 bits afin qu'ils puissent être envoyés sur le réseau. Je travaille en C++.

Je comprends combien de précision je perdrais, mais c’est acceptable pour mon application.

Le format IEEE 16 bits serait génial.

35
Matt Fichman

std::frexp extrait le significande et l’exposant des flottants ou des doubles normaux - vous devez ensuite décider quoi faire des exposants trop grands pour tenir dans un flottant semi-précis (saturer ...?), ajuster en conséquence, et mettre le nombre de demi-précision ensemble. Cet article a le code source C pour vous montrer comment effectuer la conversion.

18
Alex Martelli

Conversion complète de simple précision à demi-précision. Ceci est une copie directe de ma version SSE, elle n’a donc pas de branche. Il utilise le fait que dans GCC (-true == ~ 0), cela peut être vrai pour VisualStudio également, mais je n’ai pas de copie.

    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;
        }
    };

C'est donc beaucoup à prendre en compte, mais il gère toutes les valeurs sous-normales, les deux infinis, les NaN silencieux, les NaN de signalisation et le zéro négatif. Bien entendu, une assistance IEEE complète n’est pas toujours nécessaire. Donc, compresser les flotteurs génériques: 

    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;
        }

    };

Cela force toutes les valeurs dans la plage acceptée, pas de support pour les NaN, les infinis ou le zéro négatif. Epsilon est la plus petite valeur autorisée dans la plage. La précision est le nombre de bits de précision à retenir du float. Bien qu'il y ait beaucoup de branches ci-dessus, elles sont toutes statiques et seront mises en cache par le prédicteur de branche dans la CPU.

Bien sûr, si vos valeurs ne nécessitent pas une résolution logarithmique proche de zéro. Il est alors beaucoup plus rapide de les linéariser à un format de point fixe, comme cela a déjà été mentionné. 

J'utilise FloatCompressor (version SSE) dans la bibliothèque graphique pour réduire la taille des valeurs de couleur float linéaire en mémoire. Les flottants compressés ont l’avantage de créer de petites tables de recherche pour les fonctions qui prennent beaucoup de temps, telles que la correction gamma ou les valeurs transcendantes. La compression des valeurs sRGB linéaires réduit au maximum 12 bits ou la valeur maximale à 3011, ce qui est idéal pour une taille de table de conversion pour/de sRGB.

46
Phernost

Compte tenu de vos besoins (-1000, 1000), il serait peut-être préférable d'utiliser une représentation en virgule fixe.

//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;
}

Cela vous donnera une précision au 0,05 près. Si vous changez 20000 en SHORT_MAX, vous obtiendrez un peu plus de précision, mais certains nombres entiers deviendront des décimales à l'autre bout.

18
Artelius

Si vous envoyez un flux d'informations, vous pourriez probablement faire mieux que cela, surtout si tout se trouve dans une plage cohérente, comme cela semble être le cas de votre application.

Envoyez un petit en-tête, qui consiste uniquement en un minimum et un maximum float32. Vous pouvez ensuite envoyer vos informations sous forme d'interpolation 16 bits entre les deux. Comme vous dites également que la précision n'est pas un problème, vous pouvez même envoyer 8 bits à la fois.

Votre valeur serait quelque chose comme, au moment de la reconstruction:

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

J'espère que cela pourra aider.

-À M

5
tsalter

La plupart des approches décrites dans les autres réponses présentées ici ne résout pas correctement la conversion de float en demi, jettent les sous-normales, ce qui pose problème, car 2 ** - 14 devient votre plus petit nombre non nul, ou bien vous empêche de faire des choses malheureuses avec Inf/NaN. Inf est également un problème car le plus grand nombre fini de moitié est un peu inférieur à 2 ^ 16. OpenEXR était inutilement lent et compliqué, la dernière fois que je l'ai regardé. Une approche rapide et correcte utilisera la FPU pour effectuer la conversion, sous forme d'instruction directe ou à l'aide du matériel d'arrondi de la FPU pour obtenir les résultats escomptés. Toute conversion de moitié en float ne doit pas être plus lente qu'une table de correspondance de 2 ^ 16 éléments. 

Les suivants sont difficiles à battre:

Sous OS X/iOS, vous pouvez utiliser vImageConvert_PlanarFtoPlanar16F et vImageConvert_Planar16FtoPlanarF. Voir Accelerate.framework. 

Intel ivybridge a ajouté les instructions SSE à cet effet. Voir f16cintrin.h . Des instructions similaires ont été ajoutées au ARM ISA pour Neon. Voir vcvt_f32_f16 et vcvt_f16_f32 dans arm_neon.h. Sur iOS, vous devrez utiliser les arm64 ou armv7s Arch pour y accéder.

3
Ian Ollmann

Ce code convertit un nombre à virgule flottante de 32 bits en 16 bits et plus.

#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;
}

J'ai testé avec le Intel icpc 16.0.2:

$ icpc a.cpp

g ++ 7.3.0:

$ g++ -march=native a.cpp

et clang ++ 6.0.0:

$ clang++ -march=native a.cpp

Il imprime:

$ ./a.out
3.14159
3.14062

La documentation sur ces éléments intrinsèques est disponible à l'adresse suivante:

https://software.intel.com/en-us/node/524287

https://clang.llvm.org/doxygen/f16cintrin_8h.html

3
Ondřej Čertík

Cette question est déjà un peu ancienne, mais pour des raisons d'exhaustivité, vous pouvez également consulter cet article pour une conversion moitié-à-flottant et une conversion à la moitié.

Ils utilisent une approche basée sur des tables sans branches avec des tables de recherche relativement petites. Il est totalement conforme à IEEE et bat même les performances des routines de conversion sans embranchement conformes à IEEE de Phernost (du moins sur ma machine). Mais bien sûr, son code est beaucoup mieux adapté à SSE et n’est pas sujet aux effets de latence de la mémoire.

3
Christian Rau

Cette conversion en virgule flottante de 16 à 32 bits est assez rapide dans les cas où vous n'avez pas à comptabiliser les infinis ou les NaN et que vous pouvez accepter des valeurs de dénormalité nulle (DAZ). C'est à dire. il convient aux calculs sensibles aux performances, mais vous devez vous méfier de la division par zéro si vous prévoyez de rencontrer des dénormaux.

Notez que ceci est plus approprié pour les plates-formes x86 ou autres qui ont des déplacements conditionnels ou des équivalents "set if".

  1. Dénudez le bit de signe de l'entrée
  2. Aligner le bit le plus significatif de la mantisse sur le 22ème bit
  3. Ajuster le biais de l'exposant
  4. Mettre les bits à zéro si l'exposant d'entrée est zéro
  5. Réinsérez le bit de signe

L'inverse s'applique à la précision simple à demi, avec quelques ajouts.

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;
};

Notez que vous pouvez modifier la constante 0x7bff en 0x7c00 pour qu'elle déborde à l'infini.

Voir GitHub pour le code source.

1
awdz9nld

J'ai trouvé une implémentation de conversion du format semi-float au format single-float et retour avec using de AVX2. Il y a beaucoup plus rapide que la mise en œuvre logicielle de ces algorithmes. J'espère que cela vous sera utile.

Conversion de float 32 bits en float 16 bits:

#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);
}

Conversion de float 16 bits en float 32 bits:

#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
ErmIg

La question est ancienne et on y a déjà répondu, mais j’ai pensé qu’il serait utile de mentionner une bibliothèque C++ open source pouvant créer des floats demi-précision conformes à la norme IEEE 16 bits et dont la classe fonctionne assez 16 bits au lieu de 32. Il s’agit de la classe "demi" de la bibliothèque OpenEXR . Le code est sous licence de style BSD. Je ne crois pas qu'il y ait de dépendances en dehors de la bibliothèque standard.

0
eestrada

J'ai eu exactement le même problème et j'ai trouvé ce lien très utile. Importez simplement le fichier "ieeehalfprecision.c" dans votre projet et utilisez-le comme ceci:

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);

Je change aussi un peu de code (voir le commentaire de l'auteur (James Tursa) dans le lien):

#define INT16_TYPE int16_t 
#define UINT16_TYPE uint16_t 
#define INT32_TYPE int32_t 
#define UINT32_TYPE uint32_t
0
Coolant