2009-11-02 48 views
30

Necesito una biblioteca/algoritmo multiplataforma que convierta números de punto flotante de 32 bits a 16 bits. No necesito realizar operaciones matemáticas con los números de 16 bits; Solo necesito disminuir el tamaño de los flotadores de 32 bits para que puedan enviarse a través de la red. Estoy trabajando en C++.Conversión de punto flotante de 32 bits a 16 bits

Entiendo cuánta precisión estaría perdiendo, pero eso está bien para mi aplicación.

El formato IEEE de 16 bits sería genial.

+0

¿Seguro que serás capaz de medir el beneficio de rendimiento de esta conversión? Tendrá que enviar muchos de esos números a través del cable para compensar un ahorro significativo. Solo obtiene aproximadamente 3 dígitos decimales de precisión, y el rango tampoco es tan grande. –

+0

OTOH, la CPU es esencialmente gratuita hoy en día si puede enhebrar su programa, y ​​una transformación de una secuencia de E/S es fácilmente enhebrable.Los ahorros en E/S serán reales si el número de flotantes enviados está cerca de la capacidad de la red. Es decir. esta es una buena compensación de ancho de banda/latencia, y como tal solo relevante cuando en realidad tiene un problema de bandwitdh y no tiene problemas de latencia. – MSalters

+0

¿C++ tiene algún soporte nativo para flotadores de 16 bits? – Lazer

Respuesta

17

std::frexp extrae el significado y el exponente de los flotantes o dobles normales - entonces debe decidir qué hacer con los exponentes que son demasiado grandes para caber en un flotador de media precisión (saturar ...?), Ajustar en consecuencia, y junta el número de precisión media. This article tiene un código fuente C para mostrarle cómo realizar la conversión.

+0

En realidad, los valores que estoy enviando tienen un rango muy limitado: (-1000, 1000) por lo que el exponente no es un gran problema. –

+0

@Matt, si ** sabes ** el exponente nunca pasará por alto o por exceso, ¡entonces tu trabajo será más fácil! -) –

+0

@Alex, de hecho, ¡lo hace más fácil! Gracias. –

18

Dadas sus necesidades (-1000, 1000), quizás sería mejor usar una representación de punto fijo.

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

Esto le dará una precisión cercana a 0.05. Si cambias 20000 a SHORT_MAX obtendrás un poco más de precisión, pero algunos números enteros terminarán como decimales en el otro extremo.

+0

+1 Esto le proporcionará * mucha más * precisión que un flotador de 16 bits en casi todos los casos, y con menos matemáticas y sin casos especiales. Un flotador IEEE de 16 bits solo tendrá 10 bits de precisión y genera la mitad de sus posibles valores en el rango (-1, 1) –

+10

Depende de la distribución en el rango [-1000, 1000]. Si la mayoría de los números están de hecho en el rango [-1,1], la precisión de los flotadores de 16 bits es en promedio mejor. – MSalters

+0

Esto sería mejor con SHORT_MAX y 1024 como factor de escala, dando una representación de punto fijo de 10.6bit y todos los enteros serían exactamente representables. La precisión sería 1/2^6 = 0.015625, que es mucho mejor que 0.05, y el factor de escala de potencia de dos es fácil de optimizar a un cambio de bit (es probable que el compilador lo haga por usted). – Clifford

4

Si envía una secuencia de información, probablemente podría hacerlo mejor, especialmente si todo está en un rango constante, como su aplicación parece tener.

Envía un encabezado pequeño, que solo consta de un mínimo y un máximo de float32, luego puedes enviar tu información como un valor de interpolación de 16 bits entre los dos. Como también dices que la precisión no es un gran problema, incluso puedes enviar 8bits a la vez.

Su valor sería algo así como, en el momento de la reconstrucción:

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

Espero que ayude.

-Tom

+0

Esta es una gran solución, especialmente para valores de vector/cuaternión normalizados que usted sabe que siempre estarán en el rango (-1, 1). –

+0

+1 para usar 'numeric_limits'. – xtofl

+0

el problema con el uso de la interpolación en lugar de simplemente escalar, es que el cero no está representado exactamente y algunos sistemas son sensibles a eso, como la matriz matemática 4x4. por ejemplo, digamos (min, max-min) es (-11.356439590454102, 23.32344913482666), entonces lo más cerca que puede llegar a cero es 0.00010671140473306195. – milkplus

44

La conversión completa de precisión simple a la mitad de precisión. Esta es una copia directa de mi versión de SSE, por lo que es sin ramas. Hace uso del hecho de que en GCC (-true == ~ 0), puede ser cierto también para VisualStudio, pero no tengo una copia.

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

Así que eso es mucho para tomar en pero, maneja todos los valores subnormales, ambos infinitos, tranquila NANS, señalización NaNs, y el cero negativo. Por supuesto, el soporte IEEE completo no siempre es necesario. Así comprimir flotadores genéricos:

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

    }; 

Esto obliga a todos los valores en el rango aceptado, no hay soporte para NaNs, infinitos o cero negativo. Epsilon es el menor valor permitido en el rango. La precisión es cuántos bits de precisión retener del flotador. Si bien hay muchas ramas arriba, todas son estáticas y se almacenarán en caché mediante el predictor de bifurcación en la CPU.

Por supuesto, si sus valores no requieren una resolución logarítmica que se aproxime a cero. Luego, linealizarlos a un formato de punto fijo es mucho más rápido, como ya se mencionó.

Uso el FloatCompressor (versión SSE) en la biblioteca de gráficos para reducir el tamaño de los valores de color flotantes lineales en la memoria.Los flotadores comprimidos tienen la ventaja de crear tablas de búsqueda pequeñas para funciones que requieren mucho tiempo, como la corrección gamma o transcendentales. La compresión de valores sRGB lineales se reduce a un máximo de 12 bits o un valor máximo de 3011, lo que es excelente para un tamaño de tabla de búsqueda para/desde sRGB.

+0

Al principio, escribe que se basa en '(-true == ~ 0)' de GCC. Quiero usar su fragmento de código en Visual Studio 2012, ¿tiene un par de entrada + salida esperado que podría decirme si mi compilador hace lo correcto? Parece que se convierte y regresa sin problemas y la expresión mencionada es cierta. – Cygon

+2

¿Cuál es la licencia de su clase Float16Compressor? –

+5

The Unlicense (http://choosealicense.com/licenses/unlicense/) que es de dominio público. – Phernost

3

Esta pregunta ya es un poco antigua, pero, para completar, también puede consultar this paper para conversiones de medio a flotación y de flotación a media.

Utilizan un enfoque basado en tablas sin ramas con tablas de consulta relativamente pequeñas. Es completamente compatible con IEEE e incluso supera las prestaciones de conversión sin sucursales conforme a IEEE de Phernost en rendimiento (al menos en mi máquina). Pero, por supuesto, su código es mucho más adecuado para SSE y no es tan propenso a los efectos de latencia de memoria.

+0

+1 Este documento es muy bueno. Tenga en cuenta que no es * completamente * conforme a IEEE en la forma en que maneja NaN. IEEE dice que un número es NaN solo si se establece al menos uno de los bits de mantisa. Como el código proporcionado ignora los bits de orden inferior, algunos NaN de 32 bits se convierten erróneamente a Inf. Sin embargo, es poco probable que suceda. –

0

La pregunta es antigua y ya ha sido respondida, pero pensé que sería útil mencionar una biblioteca C++ de código abierto que puede crear flotantes de media precisión compatibles con IEEE de 16 bits y tiene una clase que funciona de forma muy similar al flotador integrado tipo, pero con 16 bits en lugar de 32. Es el "half" class of the OpenEXR library. El código está bajo una licencia permisiva de estilo BSD. No creo que tenga dependencias fuera de la biblioteca estándar.

+1

Si bien estamos hablando de librerías C++ de código abierto que proporcionan tipos de precisión media que cumplen con IEEE que actúan como los tipos de puntos flotantes incorporados tanto como sea posible, eche un vistazo a la [* media * biblioteca] (http: // half. sourceforge.net/) (descargo de responsabilidad: es de mí). –

1

Esta conversión para punto flotante de 16 a 32 bits es bastante rápida para casos en los que no tiene que representar infinitos o NaN, y puede aceptar denormals-as-zero (DAZ). Es decir. es adecuado para cálculos sensibles al rendimiento, pero debe tener cuidado con la división por cero si espera encontrarse con denormales.

Tenga en cuenta que esto es más adecuado para x86 u otras plataformas que tienen movimientos condicionales o equivalentes "set if".

  1. Strip el bit de signo de la entrada
  2. alinear el bit más significativo de la mantisa para el bit 22a
  3. Ajuste el sesgo exponente
  4. Set bits a todo ceros si el exponente de entrada es cero
  5. Re-inserción de bit de signo

Lo contrario se aplica para single-a-media-precisión, con algunas adiciones.

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

Tenga en cuenta que puede cambiar la constante 0x7bff a 0x7c00 para que se desborde hasta el infinito.

Consulte GitHub para obtener el código fuente.

+0

Probablemente quiso decir '0x80000000' en lugar de' 0x7FFFFFFF' ya que de lo contrario estaría haciendo un ABS en lugar de poner a cero. La última operación también podría escribirse como: 't1 & = 0x80000000 | (static_cast (t3 == 0) -1) '. Aunque probablemente depende de la plataforma (su sensibilidad a las fallas de predicción de bifurcación, la presencia de instrucción de asignación condicional, ...) y el compilador (su capacidad para generar el código apropiado para la plataforma en sí) cuál es mejor. Su versión puede verse mejor y más clara para alguien que no esté tan familiarizado con las operaciones binarias y las reglas de tipo * C++ *. –

+0

Gracias por detectar eso, he incorporado sus comentarios en la respuesta. –

+1

En float16, la prueba Clamp-to-max es claramente incorrecta, siempre se activa. La prueba de descarga a cero tiene el signo de comparación de la manera incorrecta. Creo * que las dos pruebas deberían ser: 't1 = (t3 <0x38800000)? 0: t1; 'y' t1 = (t3> 0x47000000)? 0x7bff: t1; ' – Frepa

0

Tuve este mismo problema y encontré this link muy útil. Solo importa el archivo "ieeehalfprecision".c" en su proyecto y utilizar de esta manera:

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

también puedo cambiar algo de código (Véase el comentario por el autor (James Tursa) en el enlace):

#define INT16_TYPE int16_t 
#define UINT16_TYPE uint16_t 
#define INT32_TYPE int32_t 
#define UINT32_TYPE uint32_t 
10

mitad a flote:
float f = ((h&0x8000)<<16) | (((h&0x7c00)+0x1C000)<<13) | ((h&0x03FF)<<13);

Flotante a la mitad:
uint32_t x = *((uint32_t*)&f);
uint16_t h = ((x>>16)&0x8000)|((((x&0x7f800000)-0x38000000)>>13)&0x7c00)|((x>>13)&0x03ff);

+3

Pero, por supuesto, tenga en cuenta que actualmente ignora cualquier tipo de desbordamiento, desbordamiento, valores desnormalizados o valores infinitos. –

3

La mayoría de los enfoques descritos en las otras respuestas aquí no redondean correctamente en la conversión de flotador a la mitad, descartan los subnormales, lo cual es un problema ya que 2 ** - 14 se convierte en el número menor que cero o hace cosas desafortunadas con Inf/NaN. Inf también es un problema porque el mayor número finito a la mitad es un poco menos de 2^16. OpenEXR fue innecesariamente lento y complicado, la última vez que lo miré. Un enfoque rápido correcto usará la FPU para hacer la conversión, ya sea como una instrucción directa, o usando el hardware de redondeo FPU para hacer lo correcto. Cualquier conversión de mitad a flotante no debe ser más lenta que una tabla de búsqueda de elementos de 2^16.

Los siguientes son difícil de superar:

en OS X/iOS, puede utilizar vImageConvert_PlanarFtoPlanar16F y vImageConvert_Planar16FtoPlanarF. Vea Accelerate.framework.

Intel ivybridge agregó instrucciones SSE para esto. Ver f16cintrin.h. Se agregaron instrucciones similares a ARM ISA para Neon. Consulte vcvt_f32_f16 y vcvt_f16_f32 en arm_neon.h. En iOS necesitarás usar el arco arm64 o armv7s para tener acceso a ellos.

2

Este código convierte un número de coma flotante de 32 bits a 16 bits y viceversa.

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

He probado con el compilador Intel icpc versión 16.0.2. Imprime:

3.14159 
3.14062 

Documentación sobre estas funciones intrínsecas está disponible en:

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

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

1

he encontrado una implementation de conversión de medio flotante a formato de un solo flotador y vuelta con el uso de AVX2. Hay mucho más rápido que la implementación de software de estos algoritmos. Espero que sea útil.

32 bits flotante a 16 bits de conversión de float:

#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-bits flotante a la conversión flotante de 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); 
} 
Cuestiones relacionadas