使用位运算符在C语言中模拟浮点数乘法

3
我需要编写一个程序来模拟浮点数乘法。对于这个程序,我们假设单精度浮点数存储在unsigned long a中。我需要使用以下运算符将存储在a中的数字乘以2:<< >> | & ~ ^ 我理解这些运算符的功能,但是我对如何实现此操作的逻辑感到困惑。任何帮助都将不胜感激。

2
请在此处编写您的第一个实现代码!您有什么疑问或问题吗? - Sir Jo Black
2
乘以2意味着需要将指数加1。为此,您需要了解浮点数的格式 - user3386109
1
你确定你不能使用加法吗?(请不要认为我考虑的是数字+数字,我理解这个问题及其解决方法) - bruno
@bruno 是的,我们不被允许使用加法 - user10992941
1
“编写一个模拟浮点数乘法的程序。” 代码需要处理所有的 float 值吗?如果不是,那么代码需要模拟哪个子集的 float 值? - chux - Reinstate Monica
2
@chux 我认为,既然我们被给予一个 unsigned long 来模拟一个只有一位精度的浮点值,那么我们应该处理所有可以模拟的情况。 - user10992941
4个回答

3

必须使用以下运算符之一将a中存储的数字乘以2:<< >> | & ~ ^

由于我们有一个无符号长整型来模拟具有单精度的浮点值,因此我们应该处理所有可以模拟的情况。ref

首先让我们假设浮点数被编码为binary32,而unsigned是32位的。C语言不需要这两个条件。

首先隔离指数以处理float子组:次正常、正常、无穷大和NAN

下面是一些经过轻微测试的代码-我稍后会进行审核,现在请将其视为伪代码模板。

#define FLT_SIGN_MASK  0x80000000u
#define FLT_MANT_MASK  0x007FFFFFu
#define FLT_EXPO_MASK  0x7F800000u
#define FLT_EXPO_LESSTHAN_MAXLVAUE(e)   ((~(e)) & FLT_EXPO_MASK)
#define FLT_EXPO_MAX   FLT_EXPO_MASK
#define FLT_EXPO_LSBit 0x00800000u

unsigned increment_expo(unsigned a) {
  unsigned carry = FLT_EXPO_LSBit;
  do {
    unsigned sum = a ^ carry;
    carry = (a & carry) << 1;
    a = sum;
  } while (carry);
  return a;
}

unsigned float_x2_simulated(unsigned x) {
  unsigned expo = x & FLT_EXPO_MASK;
  if (expo) { // x is a normal, infinity or NaN
    if (FLT_EXPO_LESSTHAN_MAXLVAUE(expo)) { // x is a normal
      expo = increment_expo(expo);  // Double the number
      if (FLT_EXPO_LESSTHAN_MAXLVAUE(expo)) { // no overflow
        return (x & (FLT_SIGN_MASK | FLT_MANT_MASK)) | expo;
      }
      return (x & FLT_SIGN_MASK) | FLT_EXPO_MAX;
    }
    // x is an infinity or NaN
    return x;
  }
  // x is a sub-normal
  unsigned m = (x & FLT_MANT_MASK) << 1;  // Double the value
  if (m & FLT_SIGN_MASK) {
    // Doubling caused sub-normal to become normal
    // Special code not needed here and the "carry" becomes the 1 exponent.
  }
  return (x & FLT_SIGN_MASK) | m;
}

UV 当然,度过了慢慢的星期天? - chqrlie
@chqrlie 正在过着梦想生活。 - chux - Reinstate Monica

1
下面的函数 fpmul_by_2() 实现了所需的功能,假设 'unsigned long' 是 32 位整数类型,'float' 是映射到 IEEE-754 'binary32' 的 32 位浮点类型。进一步假设我们要模仿禁用异常的 IEEE-754 乘法,生成标准规定的屏蔽响应。此外使用了两个帮助函数,分别实现了32位整数加法和等式比较。加法基于二进制加法中的和与进位比特的定义 (详见 this previous question),而等式比较则利用了 (a^b) == 0 等价于 a == b 的事实。
处理浮点数参数需要广泛区分三类运算对象:非规格化数和零、规格化数、无穷大和NaN。规格化数的加倍通过增加指数来实现,因为我们操作的是二进制浮点格式。可能会发生溢出,在这种情况下必须返回无穷大。无穷大和NaN不变地返回,只有SNaN被转换为QNaN,这是IEEE-754规定的掩码响应。非规格化数和零通过简单地将尾数加倍来处理。零、次规范数和无穷大的处理可能会破坏符号位,所以参数的符号位被强制赋给结果。
下面包含的测试框架对fpmul_by_2()进行了详尽测试,在现代PC上只需几分钟即可完成。我在运行Windows的x64平台上使用了英特尔编译器。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// assumptions:
// 'unsigned long' is a 32-bit type 
// 'float' maps to IEEE-754 'binary32'. Exceptions are disabled

// add using definition of sum and carry bits in binary addition
unsigned long add (unsigned long a, unsigned  long b)
{
    unsigned long sum, carry;
    carry = b;
    do {
        sum = a ^ carry;
        carry = (a & carry) << 1;
        a = sum;
    } while (carry);
    return sum;
}

// return 1 if a == b, else 0
int eq (unsigned long a, unsigned  long b)
{
    unsigned long t = a ^ b;
    // OR all bits into lsb
    t = t | (t >> 16);
    t = t | (t >>  8);
    t = t | (t >>  4);
    t = t | (t >>  2);
    t = t | (t >>  1);
    return ~t & 1;
}

// compute 2.0f * a
unsigned long fpmul_by_2 (unsigned long a)
{
    unsigned long expo_mask = 0x7f800000UL;
    unsigned long expo_lsb  = 0x00800000UL;
    unsigned long qnan_mark = 0x00400000UL;
    unsigned long sign_mask = 0x80000000UL;
    unsigned long zero      = 0x00000000UL;
    unsigned long r;

    if (eq (a & expo_mask, zero)) {             // subnormal or zero
        r = a << 1;                             // double significand
    } else if (eq (a & expo_mask, expo_mask)) { // INF, NaNs
        if (eq (a & ~sign_mask, expo_mask)) {   // INF
            r = a;
        } else {                                // NaN
            r = a | qnan_mark;                  // quieten SNaNs
        }
    } else {                                    // normal
        r = add (a, expo_lsb);                  // double by bumping exponent
        if (eq (r & expo_mask, expo_mask)) {    // overflow
            r = expo_mask;
        }
    }
    return r | (a & sign_mask);                 // result has sign of argument
}

float uint_as_float (unsigned long a)
{
    float r;
    memcpy (&r, &a, sizeof r);
    return r;
}

unsigned long float_as_uint (float a)
{
    unsigned long r;
    memcpy (&r, &a, sizeof r);
    return r;
}

int main (void)
{
    unsigned long res, ref, a = 0;
    do {
        res = fpmul_by_2 (a);
        ref = float_as_uint (2.0f * uint_as_float (a));
        if (res != ref) {
            printf ("error: a=%08lx  res=%08lx  ref=%08lx\n", a, res, ref);
            return EXIT_FAILURE;
        }
        a++;
    } while (a);
    printf ("test passed\n");
    return EXIT_SUCCESS;
}

1
@chux 你说得对。我太粗心了。现在会马上修复。 - njuffa

1
这是一个使用+运算符的简单代码。它并不打算涵盖浮点数处理的所有方面。此解决方案向您展示,将单精度浮点数的指数(位23-29,30是指数符号)加1,即可获得乘以2的结果。
此代码仅使用位运算符来考虑符号位,并避免指数溢出。
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

#include <inttypes.h>

int main()
    {
        float f = 23.45F;

        uint32_t *i=(uint32_t *)(&f);
        uint32_t app;

        printf("%08X %f\n",*i,f);

        app = *i & (0xC0000000); // copies bits 31 and 30
        *i += (1U<<23);
        *i &= ~(0xC0000000);     // leave bits 31 and 30
        *i |= app;               // set original bits 31 and 30


        printf("%08X %f\n",*i,f);

        return 0;
    }

另请参阅: 维基百科单精度浮点数


1
你不想使用 int exp = ... ; switch (exp) { ... case 23: exp = 24; break; case 24: exp = 25; break; ... } ... 或等效的代码,这太奇怪了^^ - bruno
1
_&_允许执行掩码操作。 - bruno
1
@bruno 感谢你的回答和帮助,但是Jo说得对,不允许使用加法。这就是为什么它一直让我感到困惑的原因之一,哈哈。 - user10992941
有一个使用异或和进位的解决方案!;) - Sir Jo Black
1
在接近FLT_MAX、NAN、次正规化值以及可能的0值时会失败。还存在抗锯齿问题和可移植性差的情况。 - chux - Reinstate Monica
显示剩余4条评论

1
这是使用位运算符的代码。
该代码将单精度浮点数乘以2,使浮点指数增加1,并仅使用位运算符;此外,还要注意指数和数字符号(位30和31)。
它并不打算涵盖所有浮点运算的方面。
请记住,如果代码更改了位30和/或31,则会发生溢出。
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

#include <inttypes.h>

int main()
{
    float f = -23.45F;

    uint32_t *i=(uint32_t *)(&f);
    uint32_t sgn;
    uint32_t c,sc;

    printf("%08X %f\n",*i,f);

    sgn = *i & (0xC0000000); // copies bits 31 and 30

    c = *i & (1U<<23);
    *i ^= (1U<<23);

    while(c)
    {
        sc = c << 1;
        c = *i & sc;
        *i ^= sc;
    };

    if (sgn != *i & (0xC0000000)) {
       puts("Exponent overflow");
    }

    printf("%08X %f\n",*i,f);

    return 0;
}

参见: 维基百科 单精度浮点数


很多同样的缺陷——对于楼主来说可能没什么关系,但并不是通用解决方案。 - chux - Reinstate Monica
@chux 这段代码是一个示例,旨在演示如何增加浮点数的指数,而不是涵盖浮点数管理的所有方面。 - Sir Jo Black
1
同意代码并不能涵盖所有方面,但由于答案没有表达任何限制,我进行了评论以指出各种不足之处。 - chux - Reinstate Monica

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