使用位运算(软件浮点)将整数转换为浮点数或浮点数转换为整数

12
我想知道能否帮忙解释将整数转换为浮点数或将浮点数转换为整数的过程。在我的课堂上,我们只能使用位运算符来完成这个任务,但我认为深入了解类型转换可以更好地帮助我理解这个阶段。
据我所知,将整数转换为浮点数需要将整数转换为二进制表示,通过寻找尾数、指数和分数来规范化整数值,然后从那里输出浮点数值?
至于浮点数到整数的转换,则需要将值分离成尾数、指数和分数,然后按照上述步骤反转以得到整数值?
我尝试遵循此问题的说明:Casting float to int (bitwise) in C。但我无法真正理解它。
还有,有人能解释一下为什么在将整数转换为浮点数时,大于23位的值需要进行四舍五入吗?

在C语言中,将浮点数强制转换为整数(按位)是将binary32位模式强制转换为unsigned int,而不是四舍五入到最近的整数。类型强制转换是实现软件浮点数的第一步,它通过指数将尾数移位以使小数点对齐到正确的位置。(实际上是基数点,因为这是二进制而不是十进制,所以单词“十进制”是错误的。) - Peter Cordes
如何通过位运算手动执行(float)x?如何将无符号整数转换为浮点数?如何在C语言中将浮点数强制转换为整数(位运算)? - phuclv
3个回答

21

首先,如果想更好地理解浮点数陷阱,可以考虑阅读一篇论文:“计算机科学家应了解的有关浮点运算的知识”,http://www.validlab.com/goldberg/paper.pdf

接下来是正文。

以下代码是基本的,试图从范围在0 < value < 224的无符号整数中产生一个IEEE-754单精度浮点数。这是你在原问题中最有可能遇到的格式。

IEEE-754单精度浮点数分为三个字段:一个符号位、8位指数和23位有效数字(有时称为尾数)。IEEE-754使用一个“隐藏1”有效数字,这意味着实际上总共有24位有效数字。比特按从左到右的顺序打包,其中符号位在位31中,指数在位30 .. 23中,有效数字在位22 .. 0中。以下来自Wikipedia的示意图说明了此过程:

floating point format

指数具有偏置值127,这意味着与浮点数相关联的实际指数比存储在指数字段中的值少127。因此,指数为0将被编码为127。

(注意:完整的维基百科文章可能对你有趣。参考:http://en.wikipedia.org/wiki/Single_precision_floating-point_format

因此,将IEEE-754数字0x40000000解释为以下内容:

  • 位31 = 0:正值
  • 位30 .. 23 = 0x80:指数= 128-127 = 1(又称21
  • 位22 .. 0都是0:有效数字= 1.00000000_00000000_0000000。 (请注意,我还原了隐藏的1)

因此,该值为1.0 x 21 = 2.0。

因此,要将限定范围内的无符号整数转换为IEEE-754格式,可以使用下面的函数。它采取以下步骤:

  • 将整数的前导1与浮点表示中的“隐藏”1的位置对齐。
  • 在对齐整数时,记录总移位数。
  • 屏蔽隐藏的1。
  • 使用移位数计算指数并将其附加到数字上。
  • 使用reinterpret_cast将结果的位模式转换为float。这部分是一个丑陋的hack,因为它使用了类型强制转换指针。您还可以通过滥用union来完成此操作。一些平台提供了内置操作(例如_itof),以使此重新解释更少丑陋。

有更快的方法来做到这一点; 这种方式旨在教学上有用,但不是超级高效的:

float uint_to_float(unsigned int significand)
{
    // Only support 0 < significand < 1 << 24.
    if (significand == 0 || significand >= 1 << 24)
        return -1.0;  // or abort(); or whatever you'd like here.

    int shifts = 0;

    //  Align the leading 1 of the significand to the hidden-1 
    //  position.  Count the number of shifts required.
    while ((significand & (1 << 23)) == 0)
    {
        significand <<= 1;
        shifts++;
    }

    //  The number 1.0 has an exponent of 0, and would need to be
    //  shifted left 23 times.  The number 2.0, however, has an
    //  exponent of 1 and needs to be shifted left only 22 times.
    //  Therefore, the exponent should be (23 - shifts).  IEEE-754
    //  format requires a bias of 127, though, so the exponent field
    //  is given by the following expression:
    unsigned int exponent = 127 + 23 - shifts;

    //  Now merge significand and exponent.  Be sure to strip away
    //  the hidden 1 in the significand.
    unsigned int merged = (exponent << 23) | (significand & 0x7FFFFF);


    //  Reinterpret as a float and return.  This is an evil hack.
    return *reinterpret_cast< float* >( &merged );
}
你可以使用检测数字中前导1的函数来使这个过程更加高效。(有些函数叫做“clz”表示“计算前导零”,或者“norm”表示“规范化”。)
你也可以通过记录符号,取绝对值,执行上述步骤,然后将符号放入数字的第31位来扩展到带符号的数字。
对于大于等于2的24次方的整数,整个整数无法适配32位浮点格式的有效数字字段。这就是为什么需要“舍入”的原因:你需要丢失LSB以使值适配。因此,多个整数最终会映射到相同的浮点模式上。具体的映射取决于舍入模式(向负无穷舍入,向正无穷舍入,向零舍入,向最近的偶数舍入)。但事实是,在不丢失一些数据的情况下,你不能将24位压缩到少于24位。
你可以从上面的代码中看到这一点。它通过将前导1对齐到隐藏的1位置来工作。如果一个值大于或等于2的24次方,代码将需要向移位,而这必然会移除LSB。舍入模式只是告诉你如何处理移除的位。

希望这里提供的信息足以帮助您反向进行该过程。 :-) - Joe Z
肯定有的 :) 特别是在我们关于另一个问题的聊天中。你帮了我很多,再次感谢 Joe :) - Andrew T
嗨,乔,我还有一个问题要问你。好的一面是,我相信到与有效数字进行按位与运算的那一点为止,一切都正常!所以迄今为止非常感谢你的帮助 :)然而,当我尝试将有效数字与0x7FFFFF进行按位与运算时,我收到了以下消息:“立即数0x007FFFFF无法由0-255左移0-23位表示或在所有奇数或偶数字节中重复”那么你认为我能用其他方法去掉第23位吗? - Andrew T
啊,这是ARM汇编指令中的常量限制。你必须从常量池中LDR它,或使用其他指令。你所需要做的就是清除第23位,因此BIC可能是一个合理的选择。(即 BIC ..., #0x00800000)。我最近没有编写过太多ARM汇编代码,但我认为这是有效的。 - Joe Z
我在ARM中加载十六进制值时一直有点困惑!但我使用了:"ldr r6, =0x7FFFFF","AND r0, r6",其中r0是有效数字。我相信那应该可以运行...或者至少我希望如此。我也相信您对于位清除也是正确的。当我逐步执行程序时,我的指令:"ldr r1, =1","lsl r1, 23"也变成了0x00800000 :) - Andrew T
多年后的补充:在C++20之前和所有版本的C中,应该使用memcpy()来处理最后一步中的类型转换。在C++20中,可以使用bit_cast<>。 - Joe Z

2

Joe Z的回答很优雅,但输入值的范围非常有限。32位浮点数可以存储以下范围内的所有整数值:

[-224...+224] = [-16777216...+16777216]

以及此范围之外的一些其他值。

整个范围将被覆盖:

float int2float(int value)
{
    // handles all values from [-2^24...2^24]
    // outside this range only some integers may be represented exactly
    // this method will use truncation 'rounding mode' during conversion

    // we can safely reinterpret it as 0.0
    if (value == 0) return 0.0;

    if (value == (1U<<31)) // ie -2^31
    {
        // -(-2^31) = -2^31 so we'll not be able to handle it below - use const
        // value = 0xCF000000;
        return (float)INT_MIN;  // *((float*)&value); is undefined behaviour
    }

    int sign = 0;

    // handle negative values
    if (value < 0)
    {
        sign = 1U << 31;
        value = -value;
    }

    // although right shift of signed is undefined - all compilers (that I know) do
    // arithmetic shift (copies sign into MSB) is what I prefer here
    // hence using unsigned abs_value_copy for shift
    unsigned int abs_value_copy = value;

    // find leading one
    int bit_num = 31;
    int shift_count = 0;

    for(; bit_num > 0; bit_num--)
    {
        if (abs_value_copy & (1U<<bit_num))
        {
            if (bit_num >= 23)
            {
                // need to shift right
                shift_count = bit_num - 23;
                abs_value_copy >>= shift_count;
            }
            else
            {
                // need to shift left
                shift_count = 23 - bit_num;
                abs_value_copy <<= shift_count;
            }
            break;
        }
    }

    // exponent is biased by 127
    int exp = bit_num + 127;

    // clear leading 1 (bit #23) (it will implicitly be there but not stored)
    int coeff = abs_value_copy & ~(1<<23);

    // move exp to the right place
    exp <<= 23;

    union
    {
        int rint;
        float rfloat;
    }ret = { sign | exp | coeff };

    return ret.rfloat;
}

当然,还有其他方法可以找到整数的绝对值(无分支)。同样,计算前导零也可以不使用分支来完成,所以将此示例视为示例;-)。

在C语言中,return *((float*)&ret);是未定义行为(某些编译器如MSVC会定义该行为,但其他编译器可能会失败)。请使用memcpy或union进行类型转换。 - Peter Cordes
我希望你能修正你的答案,并让未来的读者知道。顺便说一句,在这种情况下使用 unsigned int(或更好的是 uint32_t)来移位/ OR FP 位模式也是一个好主意。左移有符号整数会改变符号位,这在技术上是有符号溢出 UB,我想。 (你实际上并没有这样做,而且还有足够多的其他假设,如二进制补码和 32 位 int,可能并不重要。) - Peter Cordes
另外,有符号值的右移是实现定义的,而不是未定义的。 - S.S. Anne
@PeterCordes 你应该意识到 return (float)INT_MIN; 是无意义的,因为这只是将 int 转换为 float。此外,如果值是 unsigned,它可以在代码中处理。 - S.S. Anne
这并不是毫无意义,它返回正确的FP值,就像0.0使用FP常量来返回所有零比特模式一样。两者都需要编译器在编译时知道如何生成FP比特模式。如果写成-2147483648.0f可能会让你更加满意,而不是包含可能运行时的int->float转换并具有循环依赖性?注释仍然显示实际的比特模式。 - Peter Cordes

2

您查看了IEEE 754浮点表示吗?

在32位归一化形式中,它有(尾数的)符号位、8位指数(过量-127,我想)和23位尾数在“十进制”中,除去“0.”(始终以此形式出现),基数为2,而不是10。也就是说:最高有效位的值为1/2,下一位的值为1/4,依此类推。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接