Conversión de coma flotante de 32 bits a 16 bits

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

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.

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 pon el número de media precisión juntos. Este artículo tiene un código fuente C para mostrarle cómo realizar la conversión.

Conversión completa de precisión simple a media precisión. Esta es una copia directa de mi versión de SSE, por lo que es sin twigs. 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; vf = value; uint32_t sign = v.si & signN; v.si ^= sign; sign >>= shiftSign; // logical shift s.si = mulN; s.si = sf * vf; // 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; sf *= v.si; int32_t mask = -(norC > v.si); v.si < <= shift; v.si ^= (s.si ^ v.si) & mask; v.si |= sign; return vf; } }; 

Así que eso es mucho que asimilar, pero maneja todos los valores subnormales, ambos infinitos, NaN silenciosos, NaN de señalización y cero negativo. Por supuesto, el soporte IEEE completo no siempre es necesario. Entonces comprimiendo flotadores generics:

  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; vf = min; _minF = v.si; vf = epsilon; _epsF = v.si; vf = 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; vf = 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 vf; } uint32_t compress(float value) { Bits v; vf = 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 vf; } }; 

Esto fuerza a todos los valores al rango aceptado, sin soporte para NaN, 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 twigs 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 cercana a cero. Luego, linealizarlos a un formato de punto fijo es mucho más rápido, como ya se mencionó.

Uso FloatCompressor (versión SSE) en la biblioteca de gráficos para reducir el tamaño de los valores de color de flotación lineal 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.

Dadas tus 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.

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

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

Si está enviando 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íe un encabezado pequeño, que solo consta de un mínimo y máximo de float32, luego puede enviar su 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::max(); // With casting, naturally ;) float val = h.min + t * (h.max - h.min); 

Espero que ayude.

-Tom

Esta pregunta ya es un poco antigua, pero para mayor información, también puede echar un vistazo a este documento para la conversión de medio a flotación y de flotación a media.

Utilizan un enfoque basado en tablas sin twigs 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.

La mayoría de los enfoques descritos en las otras respuestas aquí no se redondean correctamente en la conversión de flotador a la mitad, tiran los subnormales, lo cual es un problema ya que 2 ** – 14 se convierte en tu número más pequeño distinto de cero, o desafortunadamente con Inf / Yaya. 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íciles de superar:

En OS X / iOS, puede usar 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.

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

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

Probé con el comstackdor Intel icpc versión 16.0.2. Imprime:

 3.14159 3.14062 

La documentación sobre estos intrínsecos está disponible en:

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

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

Esta conversión de coma flotante de 16 a 32 bits es bastante rápida para los casos en los que no es necesario tener en cuenta 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. Quite el signo de la entrada
  2. Alinee el bit más significativo de la mantisa con el bit 22
  3. Ajusta el sesgo del exponente
  4. Establezca los bits a todo cero si el exponente de entrada es cero
  5. Vuelva a insertar el bit de signo

Lo contrario se aplica para precisión de una a media, 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.

Ver GitHub para el código fuente.

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

Conversión de flotación de 32 bits a flotación de 16 bits:

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

Flotación de 16 bits a la conversión de flotación de 32 bits:

 #include = 8); size_t fullAlignedSize = size&~(32-1); size_t partialAlignedSize = size&~(8-1); size_t i = 0; for (; i < fullAlignedSize; i += 32) { Float16ToFloat32(src + i + 0, dst + i + 0); Float16ToFloat32(src + i + 8, dst + i + 8); Float16ToFloat32(src + i + 16, dst + i + 16); Float16ToFloat32(src + i + 24, dst + i + 24); } for (; i < partialAlignedSize; i += 8) Float16ToFloat32(src + i, dst + i); if (partialAlignedSize != size) Float16ToFloat32(src + size - 8, dst + size - 8); } 

La pregunta es antigua y ya ha sido respondida, pero calculé 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 una clase que actúa de forma muy similar al tipo flotante integrado, pero con 16 bits en lugar de 32. Es la clase “media” de la biblioteca OpenEXR . El código está bajo una licencia permisiva de estilo BSD. No creo que tenga dependencias fuera de la biblioteca estándar.

Tuve el mismo problema y encontré este enlace muy útil. Simplemente importe el archivo “ieeehalfprecision.c” en su proyecto y úselo 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 cambio un código (ver el comentario del 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