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.
¿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. –
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
¿C++ tiene algún soporte nativo para flotadores de 16 bits? – Lazer