【问题标题】:What happens to the bits of a float during division?除法期间浮点数的位会发生什么变化?
【发布时间】:2018-03-16 13:58:45
【问题描述】:

我有一个家庭作业,要求我在 C 中使用按位运算将一个 32 位单精度浮点整数除以 2(也可以使用 if 语句和 for 循环)。浮点数的位表示为无符号整数,因此我们可以使用按位运算符修改它们。我的问题是我无法理解在除法过程中位到底发生了什么。我最初的计划只是将指数位右移 1,同时保持符号位和尾数位相同,但这没有奏效。例如,当我的函数被赋予由 0x800000 表示的位时,我的函数返回 0x00000000,因为右移指数会导致所有位为 0。但是,根据作业的测试驱动程序,这种情况下的正确答案是0x00400000。这真的让我很困惑,因为我不确定指数位如何或为什么会看似转移到尾数位。

unsigned divideFloatBy2(unsigned uf){
//copy the sign bit
unsigned signBit = (1 << 31) & uf;

//copy mantissa
unsigned mantissa = ~0;
mantissa >>= 9;
mantissa &= uf;

//copy exponent
unsigned mask = 0xFF;
mask <<= 23;
unsigned exponent = (uf & mask);
exponent >>= 23;

exponent >>= 1; //right shift to divide by 2;

exponent <<= 24;

//combine all again
unsigned ret = signBit | exponent | mantissa;
return ret; //will be interpreted as float later
}

此函数对某些输入(但不是全部)正常工作,例如上面给出的输入。请记住,我更多的是询问浮点数在除法期间会发生什么,而不是简单地询问代码以使其工作。

【问题讨论】:

    标签: c floating-point bit-manipulation bitwise-operators bit


    【解决方案1】:

    您有一个很好的见解,即通过 2 的幂缩放归一化的、基数为 2 的浮点数仅影响指数(假设您既没有上溢也没有下溢),但是您执行了错误的操作。将指数右移 1 相当于将其 - 指数 - 除以 2。结果与原始数字的平方根大小相同。这根本不是你想要的,除非原来的数字是 4 左右。

    它可能会帮助你用二进制科学记数法写出一个例子,因为这与机器表示非常对应。那么,假设您的原始数字 N 是 1.01010x2110

    N / 2 = N * 2-1 = 1.01010x2110 * 2-1 = 1.01010x2110-1 = 1.01010x2101

    所以是的,尾数和符号不会改变,但对指数的影响只是将其减少 1。


    关于您的原始程序,请注意它实际上并没有正确实现您描述的方法。它将指数位向右移动 23 以将最低有效位带到单位位置,然后再向右移动一位以实现您的操作,但随后它向左移动 24 位。它应该只左移 23 位,反转原来的右移,将结果位带回正确的位置。

    您实际执行的操作的效果是清除最低有效指数位,当偏置指数为奇数时,这恰好相当于减 1。这就是为什么它有一半的时间会产生正确的答案。

    【讨论】:

    • 可能值得指出的是,通过简单的指数递减将 IEEE-754 binary32 值除以 2 仅适用于标准化浮点数。额外的缩放对于正确处理非正规(次正规)是必要的。
    • 要点,@njuffa。我已经对我的 cmets 进行了限定,以澄清它们适用于标准化输入。
    【解决方案2】:

    当 ... 给定 0x800000 时,我的函数返回 0 ....,正确答案 ... 是 0x00400000。

    这是将最小 正常 float 值除以 2,在下面的 #3 中有详细说明。


    代码有很多问题。

    1. 对于大多数有限数,递减而不是移动指数是正确的,正如 @John Bollinger 好的答案所指出的那样当指数大于 1 时

    2. exponent == 0 时,数字为sub-normal(或非正规),需要将其mantissa 字段右移(/2)。指数保持为 0。如果移出的位为 1,则除以 2 不准确。然后,根据四舍五入,mantissa 被调整 - 可能是通过添加 1。

    3. exponent == 1时,结果将是次正常的,需要在mantissa字段中创建隐含的正常数字位并右移(/2)。如上所述,这种转变可能会导致四舍五入。指数变为 0。请注意,“舍入”mant 可能会超过 mant 的最大值 0x7FFFFF,然后需要对字段进行调整。

    4. exponent == MAX (255) 时,数字不是有限的(它是无穷大或Not-a-Number),应该单独保留。

    5. 1 &lt;&lt; 31 之类的代码最好定义为:

      // unsigned signBit = (1 << 31) & uf;
      unsigned signBit = (1u << 31) & uf;   // Use an unsigned mask
      unsigned signBit = (1LU << 31) & uf;  // unsigned may be 16 bit.
      // or better yet
      unsigned signBit = uf & 0x80000000;
      
    6. mantissa 派生的角落弱点在于它依赖于(非常常见的)2 的补码。便携式替代品:

      // unsigned mantissa = ~0;  Incorrect mask in `mantissa` when `int` is not 2's comp.
      // unsigned mantissa = -1;  correct all bits set.
      // mantissa >>= 9;
      // mantissa &= uf;
      // or simply use
      unsigned mantissa = 0x7FFFFF & uf;
      

    unsigned 可能是 16、32、64 位等。最好使用最小或精确宽度类型。

    #define SIGN_MASK 0x80000000
    #define EXPO_MASK 0x7F800000
    #define MANT_MASK 0x007FFFFF
    
    #define EXPO_SHIFT 23
    #define EXPO_MAX         (EXPO_MASK >> EXPO_SHIFT)
    #define MANT_IMPLIED_BIT (MANT_MASK + 1u)
    
    uint32_t divideFloatBy2(uint32_t uf){
      unsigned sign = uf & SIGN_MASK;
      unsigned expo = uf & EXPO_MASK;
      unsigned mant = uf & MANT_MASK;
    
      expo >>= EXPO_SHIFT;
      // when the number is not an infinity nor NaN
      if (expo != EXPO_MAX) {
        if (expo > 1) {
          expo--;  // this is the usual case
        } else {
          if (expo == 1) {
            mant |= MANT_IMPLIED_BIT;
          }
          expo = 0;
          unsigned round_bit = mant & 1;
          mant /= 2;
    
          if (round_bit) {
            TBD_CODE_Handle_Rounding(round_mode, sign, &expo, &mant);
          }
        }
        expo <<= EXPO_SHIFT;
        uf = sign | expo | mant;
      }
      return uf;
    }
    

    OP 后来评论了exponent ,sign 0, mantissa == 0x3, expected result is 0x2, but my returning 1.,所以舍入模式可能是FE_TONEARESTFE_UPWARD

    expo &lt;= 1 跟随时重写案例。它是经过测试的代码 - 经历了许多 232 组合和 4 种舍入模式。

    请注意,当some_float/2.0f 计算时,它可能会影响浮点环境 状态位。我最初也是这样做的,但后来从这篇文章中删除了该代码 - 如果有兴趣,请联系。

        } else {
          if (expo == 1) {
            expo = 0;
            mant |= MANT_IMPLIED_BIT;
          }
          // Divided by 2 result inexact?
          if (mant % 2) {
            mant /= 2;
            // Determine how to round
            switch (fegetround()) {
              case FE_DOWNWARD:
                if (sign) mant++;
                break;
              case FE_TOWARDZERO:
                break;
              case FE_UPWARD:
                if (!sign) mant++;
                break;
              default: // When mode is not known, act like FE_TONEAREST
                // fall through
              case FE_TONEAREST:
                if (mant & 1) mant++;
                break;
            }
            if (mant >= MANT_IMPLIED_BIT) {
              mant = 0;
              expo++;
            }
          } else {
            mant /= 2;
          }
        }
    

    有关舍入模式的详细信息,请搜索FE_... 宏或here

    【讨论】:

    • 在这种情况下舍入到底是什么意思?即,如果 round_bit 为真,TBD_CODE_Handle_Rounding() 会做什么?
    • @jburn7 它的代码留给你写。正如此处回答的那样,“可能通过将 1”添加到 mant。您的帖子未指定应如何处理舍入。要处理所有舍入模式(在least 4 处)需要更多代码。如果帖子清楚地说明了四舍五入的目标,那就更好了。
    • 哦,我现在明白了。我们没有被告知如何舍入,除了我们需要执行 0.5*f 的按位等效,其中 f 是 32 位单精度浮点数。作为参考,我有另一个例子,当指数和符号位全为 0,尾数 == 0x3,预期结果为 0x2,但我的函数返回 0x1。但是,我仍然不能完全确定尾数位在十进制数中的含义,所以我不确定这个例子是用哪种方式舍入的。
    • @jburn7 在您的example 中,值向上舍入。 +0x3 ​​除以 2 将是 +1.5,但对于整数数学,四舍五入时变为 +2。研究FE_TONEAREST 了解更多详情。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-16
    • 2013-06-04
    • 2010-11-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多