为什么我们将归一化分数乘以0.5以获得IEEE 754表示中的有效数?

时间:2016-10-18 03:33:00

标签: c floating-point ieee-754

我对Section 7.4 of Beej's Guide to Network Programming中定义的pack754()函数有疑问。

此函数将浮点数f转换为其IEEE 754表示形式,其中bits是表示数字的总位数,expbits是用于表示的位数只有指数。

我只关注单精度浮点数,因此对于这个问题,bits指定为32expbits指定为8。这意味着23位用于存储有效位数(因为一位是符号位)。

我的问题是这行代码。

    significand = fnorm * ((1LL<<significandbits) + 0.5f);

+ 0.5f在此代码中的作用是什么?

以下是使用此功能的完整代码。

#include <stdio.h>
#include <stdint.h> // defines uintN_t types
#include <inttypes.h> // defines PRIx macros

uint64_t pack754(long double f, unsigned bits, unsigned expbits)
{
    long double fnorm;
    int shift;
    long long sign, exp, significand;
    unsigned significandbits = bits - expbits - 1; // -1 for sign bit

    if (f == 0.0) return 0; // get this special case out of the way

    // check sign and begin normalization
    if (f < 0) { sign = 1; fnorm = -f; }
    else { sign = 0; fnorm = f; }

    // get the normalized form of f and track the exponent
    shift = 0;
    while(fnorm >= 2.0) { fnorm /= 2.0; shift++; }
    while(fnorm < 1.0) { fnorm *= 2.0; shift--; }
    fnorm = fnorm - 1.0;

    // calculate the binary form (non-float) of the significand data
    significand = fnorm * ((1LL<<significandbits) + 0.5f);

    // get the biased exponent
    exp = shift + ((1<<(expbits-1)) - 1); // shift + bias

    // return the final answer
    return (sign<<(bits-1)) | (exp<<(bits-expbits-1)) | significand;
}

int main(void)
{
    float f = 3.1415926;
    uint32_t fi;

    printf("float f: %.7f\n", f);

    fi = pack754(f, 32, 8);
    printf("float encoded: 0x%08" PRIx32 "\n", fi);

    return 0;
}

+ 0.5f在此代码中的用途是什么?

2 个答案:

答案 0 :(得分:3)

代码是四舍五入的错误尝试。

long double fnorm;
long long significand;
unsigned significandbits
...
significand = fnorm * ((1LL<<significandbits) + 0.5f);  // bad code

不正确的第一个线索是f的{​​{1}},表示0.5f,是一个无意义的介绍,在float例程中指定floatlong double ffnorm数学在函数中没有应用程序。

然而,添加float并不意味着代码仅限于0.5f中的float数学。请参阅(1LL<<significandbits) + 0.5f,这可能允许更高精度的中间结果,并且在测试中欺骗了代码作者。

舍入尝试确实有意义,因为参数为FLT_EVAL_METHOD且目标表示更窄。添加long double是一种常见的方法 - 但这并不是在这里完成的。国际海事组织,作者缺乏关于0.5的评论,暗示意图明显是#34; - 虽然不正确,但不是微妙的。

作为commented,移动0.5f更接近于舍入是正确的,但可能会误导某些人认为添加是使用0.5数学完成的,(它是{{ 1}}数学将float产品添加到long double会导致long double首先提升为float

0.5f

要在不调用long double之类的首选// closer to rounding but may mislead significand = fnorm * (1LL<<significandbits) + 0.5f; // better significand = fnorm * (1LL<<significandbits) + 0.5L; // or 0.5l or simply 0.5 轮例程的情况下进行舍入,添加显式类型0.5仍然是舍入的弱尝试。它很弱,因为它在很多情况下都是错误的。 +0.5技巧依赖于 exact 的总和。

考虑

<math.h>
在截断/分配到rintl(), roundl(), nearbyintl(), llrintl()之前,

long double product = fnorm * (1LL<<significandbits); long long significand = product + 0.5; // double rounding? 本身可能会进行舍入 - 实际上是double rounding

最好在标准库函数的C棚中使用正确的工具。

product + 0.5

此四舍五入是一个极端情况,long long现在太大了,significand = llrintl(fnorm * (1ULL<<significandbits)); 需要调整。同样由@Nayuki标识,代码也有其他缺点。此外,它在significand上失败。

答案 1 :(得分:2)

+ 0.5f在代码中没有用处,可能有害或误导。

表达式(1LL<<significandbits) + 0.5f会产生float。但即使对于单精度浮点的significandbits = 23小例,表达式的计算结果为(float)(2 23 + 0.5),其精确到2 23 (连续一半)。

+ 0.5f替换+ 0.0f会导致相同的行为。哎呀,完全放弃这个术语,因为fnorm无论如何都会导致*的右侧参数被转换为long double。这将是重写该行的更好方法:long long significand = fnorm * (long double)(1LL << significandbits);

旁注:pack754()的这种实现正确处理零(并将负零折叠为正零),但错误处理次正规数(错误位),无穷大(无限循环)和NaN(错误位)。最好不要将其视为参考模型函数。