将32位无符号整数精确转换为(-1; 1)范围内的浮点数

时间:2019-06-20 20:09:55

标签: c++ c algorithm cuda floating-point

根据articles like this,一半的浮点数在间隔[-1,1]中。您能否建议如何利用这一事实,以将32位无符号整数的天真转换替换为浮点数(同时保持均匀分布)?

原始代码:

uint32_t i = /* randomly generated */;
float f = (float)i / (1ui32<<31) - 1.0f;

这里的问题是,首先将数字i转换为float,最多损失8位低位精度。只有这样,数字才会缩放到[0; 2)间隔,然后再缩放到[-1; 1)间隔。

如果知道,请提出针对x86_64 CPU或CUDA的C或C ++解决方案。

更新:带有double的解决方案对于x86_64很好,但是在CUDA中太慢了。对不起,我没想到会有这样的答复。有什么想法可以在不使用双精度浮点的情况下实现这一目标吗?

5 个答案:

答案 0 :(得分:2)

您可以改用double进行计算,这样就不会对uint32_t值造成任何精度损失,然后将结果分配给float

float f = (double)i / (1ui32<<31) - 1.0;

答案 1 :(得分:1)

如果您删除了均匀分布约束,则仅在32位整数算术上可行:

//---------------------------------------------------------------------------
float i32_to_f32(int   x)
    {
    int exp;
    union _f32          // semi result
        {
        float f;        // 32bit floating point
        DWORD u;        // 32 bit uint
        } y;
    // edge cases
    if (x== 0x00000000) return  0.0f;
    if (x< -0x1FFFFFFF) return -1.0f;
    if (x> +0x1FFFFFFF) return +1.0f;
    // conversion
    y.u=0;                              // reset bits
    if (x<0){ y.u|=0x80000000; x=-x; }  // sign (31 bits left)
    exp=((x>>23)&63)-64;                // upper 6 bits -> exponent -1,...,-64 (not 7bits to avoid denormalized numbers)
    y.u|=(exp+127)<<23;                 // exponent bias and bit position
    y.u|=x&0x007FFFFF;                  // mantissa
    return y.f;
    }
//---------------------------------------------------------------------------
int f32_to_i32(float x)
    {
    int exp,man,i;
    union _f32          // semi result
        {
        float f;        // 32bit floating point
        DWORD u;        // 32 bit uint
        } y;
    // edge cases
    if (x== 0.0f) return  0x00000000;
    if (x<=-1.0f) return -0x1FFFFFFF;
    if (x>=+1.0f) return +0x1FFFFFFF;
    // conversion
    y.f=x;
    exp=(y.u>>23)&255; exp-=127;        // exponent bias and bit position
    if (exp<-64) return 0.0f;
    man=y.u&0x007FFFFF;                 // mantissa
    i =(exp<<23)&0x1F800000;
    i|= man;
    if (y.u>=0x80000000) i=-i;          // sign
    return i;
    }
//---------------------------------------------------------------------------

我选择仅使用29位+符号=〜30位整数,以避免非规范化的数字混乱,而我却懒得编码(这会使您达到30位甚至31位,但要慢得多且复杂得多)。

但是分布不是线性的也不是均匀的:

linearity

红色中的

是范围float中的<-1,+1>,蓝色是范围integer中的<-1FFFFFFF,+1FFFFFFF>

另一方面,两次转换都没有舍入...

PS。我认为也许可以通过对6位指数(64个值)使用预先计算的 LUT 对结果进行某种程度的线性化。

答案 2 :(得分:1)

要实现的事情是,(float)i确实失去了8位精度(因此它具有24位精度),结果也只有24位精度。因此,这种精度损失不一定是一件坏事(实际上更复杂,因为如果i较小,则损失少于8位。但是事情会顺利进行的。)

因此,我们只需要固定范围,就可以将原来的非负值映射到INT_MIN..INT_MAX

此表达式有效:(float)(int)(value^0x80000000)/0x80000000

这是它的工作方式:

  1. (int)(value^0x80000000)部分翻转符号位,因此0x0被映射到INT_MIN,而0xffffffff被映射到INT_MAX
  2. 然后将转换为float。这是一些取整的地方,我们会失去精度(但这不是问题)。
  3. 然后只需除以0x80000000即可进入范围[-1..1]。由于该除法只是调整指数部分,因此该除法不会损失任何精度。

因此,只有一个舍入,其他操作不会失去精度。这些操作链应具有相同的效果,例如以无限精度计算结果,然后对float进行舍入(此理论舍入与在步骤2进行舍入具有相同的效果)。

但是,绝对可以肯定,我已经用蛮力验证了所有32位值,该表达式产生的值与(float)((double)value/0x80000000-1.0)相同。

答案 3 :(得分:0)

我建议(如果您想避免除法,并使用精确的浮点数表示的起始值1.0 * 2 ^ -32):

float e = i * ldexp(1.0,-32) - 1.0;

答案 4 :(得分:0)

  

任何想法如何在不使用双精度浮点的情况下实现这一目标?

无需过多假设float的内部:

u移至最高有效位,将float转换值减半。

“保持均匀分布”

uint32_t值的50%将位于[0.5 ... 1.0)
uint32_t值的25%将位于[0.25 ... 0.5)
uint32_t值的12.5%将位于[0.125 ... 0.25)
uint32_t值的6.25%将在[0.0625 ... 0.125)中
...

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

float ui32to0to1(uint32_t u) {
  if (u) {
    float band = 1.0f/(1llu<<32);
    while ((u & 0x80000000) == 0) {
      u <<= 1;
      band *= 0.5f;
    }
    return (float)u * band;
  }
  return 0.0f;
}

一些测试代码可以显示与double等效的功能。

int test(uint32_t u) {
  volatile float f0 = (float) ((double)u / (1llu<<32));
  volatile float f1 = ui32to0to1(u);
  if (f0 != f1) {
    printf("%8lX %.7e %.7e\n", (unsigned long) u, f0, f1);
    return 1;
  }
  return 0;
}

int main(void) {
  for (int i=0; i<100000000; i++) {
    test(rand()*65535u ^ rand());
  }
  return 0;
}

可以进行各种优化,尤其是在假设属性float的情况下。但是,对于最初的答案,我将坚持使用通用方法。

为了提高效率,循环仅需要从32迭代到FLT_MANT_DIG,通常为24。

float ui32to0to1(uint32_t u) {
  float band = 1.0f/(1llu<<32);
  for (int i = 32; (i>FLT_MANT_DIG && ((u & 0x80000000) == 0)); i--) {
    u <<= 1;
    band *= 0.5f;
  }
  return (float)u * band;
}

此答案将[0到2 32 -1]映射到[0.0到1.0)

要映射到[0至2 32 -1]至(-1.0至1.0)。它可以形成-0.0。

if (u >= 0x80000000) {
  return ui32to0to1((u - 0x80000000)*2);
} else
  return -ui32to0to1((0x7FFFFFFF - u)*2);
}